mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 23:16:46 +00:00
feat(native): the apps load the server's own UI
The apps ship only an address screen now. Once a server is picked, the native side points the WebView at it: TrekBridgeViewController on iOS and a config copy with server.url on Android. The UI then comes from that server, in the version it runs and on its own origin, so the session cookie, uploads, /ws and plugins work exactly as in a browser and no app release has to keep pace with the servers out there. The address screen (native-shell/, built into native-dist/) probes /api/health/features through native HTTP, tries https before http when no scheme was typed, and refuses servers without the nativeApp flag. It also serves as the error page when the remembered server does not load: retry, change server, or on iOS the stored copy of the server's start page. TrekShell is the app's own plugin for all of that: remember, change and reload the server, sign in through ASWebAuthenticationSession or a Custom Tab, and on iOS keep and open the offline copy of the start page, because WKWebView runs no service worker for a domain the app does not list. Plugins the frame does not use are gone (push, camera, geolocation, calendar, file transfer, in-app browser and more): the WebView covers the camera and location itself, and push needs a relay before it is worth shipping. With them go the push entitlement, the calendar usage strings and the Firebase bits. The iOS scheme falls back to capacitor://, since Capacitor drops an iosScheme of https, and the arm64 capability replaces armv7.
This commit is contained in:
@@ -35,6 +35,7 @@ dependencies {
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation "androidx.browser:browser:$androidxBrowserVersion"
|
||||
implementation project(':capacitor-android')
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
|
||||
@@ -10,23 +10,11 @@ android {
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-browser')
|
||||
implementation project(':capacitor-calendar')
|
||||
implementation project(':capacitor-camera')
|
||||
implementation project(':capacitor-file-transfer')
|
||||
implementation project(':capacitor-filesystem')
|
||||
implementation project(':capacitor-geolocation')
|
||||
implementation project(':capacitor-haptics')
|
||||
implementation project(':capacitor-inappbrowser')
|
||||
implementation project(':capacitor-keyboard')
|
||||
implementation project(':capacitor-local-notifications')
|
||||
implementation project(':capacitor-network')
|
||||
implementation project(':capacitor-preferences')
|
||||
implementation project(':capacitor-push-notifications')
|
||||
implementation project(':capacitor-share')
|
||||
implementation project(':capacitor-splash-screen')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capacitor-toast')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -24,16 +24,17 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deep links (@capacitor/app appUrlOpen).
|
||||
<!-- Deep links.
|
||||
|
||||
The scheme is @string/custom_url_scheme (com.liketrek.trek).
|
||||
launchMode is singleTask above, so a link delivers into the
|
||||
running task via onNewIntent rather than starting a second copy
|
||||
of the app on top of the first.
|
||||
|
||||
This is the return path for native OIDC/OAuth login: the flow
|
||||
opens in a Custom Tab and the provider redirects back to
|
||||
com.liketrek.trek://… , which lands here.
|
||||
This is the return path for SSO and passkey sign-in: TrekShell
|
||||
opens the server's login page in a Custom Tab, and after the
|
||||
hand-off it redirects to com.liketrek.trek://auth?code=…, which
|
||||
lands here and in TrekShellPlugin.handleOnNewIntent.
|
||||
|
||||
Not an App Link (autoVerify + https). App Links are verified
|
||||
against a specific domain via assetlinks.json, and TREK is
|
||||
@@ -59,101 +60,21 @@
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
|
||||
<!-- Notification icon, shared by @capacitor/push-notifications (via this
|
||||
FCM meta-data) and @capacitor/local-notifications (via the smallIcon
|
||||
option in capacitor.config.ts).
|
||||
|
||||
It must be a white silhouette on transparency; the launcher icon is a
|
||||
full-colour square and Android would render it as a white blob.
|
||||
notification_icon is generated from public/icons/icon-white.svg by
|
||||
scripts/generate-native-icons.mjs.
|
||||
|
||||
default_notification_channel_id names the channel FCM uses for any
|
||||
message that does not carry its own. The channel itself still has to
|
||||
be created at runtime with PushNotifications.createChannel() using
|
||||
this exact id — Android drops notifications for a channel that was
|
||||
never created, and FCM silently falls back to its own channel.
|
||||
|
||||
No default_notification_color on purpose. TREK's palette is neutral
|
||||
and user-selectable, so there is no one brand colour to tint with;
|
||||
left unset, Android tints the small icon with the system accent,
|
||||
which stays legible in both the light and dark shade. -->
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/notification_icon" />
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="@string/default_notification_channel_id" />
|
||||
|
||||
<!-- Asks Play services to install the backported Android photo picker on
|
||||
Android 11/12 devices, so @capacitor/camera gets the modern picker
|
||||
instead of falling back to ACTION_OPEN_DOCUMENT. Disabled by design:
|
||||
it is a dependency declaration, not a service that runs.
|
||||
|
||||
Drop this block if TREK ever ships a Play-services-free build
|
||||
(F-Droid); the fallback picker keeps working without it. -->
|
||||
<service
|
||||
android:name="com.google.android.gms.metadata.ModuleDependencies"
|
||||
android:enabled="false"
|
||||
android:exported="false"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="photopicker_activity:0:required" android:value="" />
|
||||
</service>
|
||||
</application>
|
||||
|
||||
<!-- Permissions.
|
||||
|
||||
Declared here only where the app itself needs them. Plugins that ship
|
||||
their own manifest contribute theirs through the merge and are NOT
|
||||
repeated: @capacitor/haptics (VIBRATE), @capacitor/calendar
|
||||
(READ_CALENDAR, WRITE_CALENDAR) and @capacitor/local-notifications
|
||||
(RECEIVE_BOOT_COMPLETED, WAKE_LOCK, SCHEDULE_EXACT_ALARM). -->
|
||||
<!-- Permissions. Only what the WebView itself needs: the file picker's
|
||||
camera option and navigator.geolocation for "locate me" on the map,
|
||||
both granted at runtime through Capacitor's WebChromeClient. -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- Reachability: connectivity.ts probes the server on an interval, and on
|
||||
native we want to skip the probe entirely when there is no transport.
|
||||
@capacitor/network declares this too; kept explicit because
|
||||
connectivity.ts depends on it directly. -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<!-- Android 13+ gates notifications behind a runtime grant. firebase-messaging
|
||||
and @capacitor/local-notifications declare this too, but relying on a
|
||||
transitive manifest merge for a permission the app must request by hand is
|
||||
a silent-failure waiting to happen — it must be requested via
|
||||
requestPermissions() before register()/schedule(). -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<!-- @capacitor/geolocation. Declares no permissions of its own, so both are
|
||||
required here. COARSE is listed first because it is the weaker grant and
|
||||
is enough for "what city am I in" — request FINE only where the map
|
||||
genuinely needs it. -->
|
||||
<!-- COARSE is listed first because it is the weaker grant and is enough
|
||||
for "what city am I in". -->
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<!-- required="false" on purpose: TREK is a planner, and everything except
|
||||
"locate me" works without GPS hardware. Marking it required would have
|
||||
Play hide the app from tablets and other GPS-less devices. -->
|
||||
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
|
||||
|
||||
<!-- @capacitor/camera. Not strictly required — the plugin launches the system
|
||||
camera activity, which carries its own permission — but io.ionic.libs's
|
||||
inappbrowser transitively declares CAMERA anyway, and once it is declared
|
||||
Android routes the plugin through the runtime-grant path. Declared here
|
||||
explicitly so it is a decision rather than something inherited. -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<!-- Stripped from the merge. io.ionic.libs:ioninappbrowser-android declares
|
||||
RECORD_AUDIO and MODIFY_AUDIO_SETTINGS so that pages opened in its WebView
|
||||
mode can use getUserMedia for microphone capture. TREK records no audio
|
||||
anywhere, and shipping these would put "Microphone" on the store listing
|
||||
and in the app's permission screen for a capability that does not exist.
|
||||
|
||||
Revisit only if a feature genuinely needs mic input in an embedded page. -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" tools:node="remove" />
|
||||
|
||||
<!-- Deliberately NOT declared: READ/WRITE_EXTERNAL_STORAGE.
|
||||
@capacitor/camera needs storage permissions only for saveToGallery, and
|
||||
TREK uploads photos to the server rather than writing them to the device
|
||||
gallery. Add them the day a feature actually needs them. -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
</manifest>
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
package com.liketrek.trek;
|
||||
|
||||
import android.os.Bundle;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
public class MainActivity extends BridgeActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
registerPlugin(TrekShellPlugin.class);
|
||||
// With a server picked, the WebView loads that server instead of the
|
||||
// bundled address screen, so the UI arrives in the version the server
|
||||
// runs and on its own origin. BridgeActivity reads `config` while it
|
||||
// builds the bridge inside super.onCreate().
|
||||
String server = TrekServerStore.getUrl(this);
|
||||
if (server != null) {
|
||||
config = TrekServerStore.configFor(this, server);
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.liketrek.trek;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import com.getcapacitor.CapConfig;
|
||||
import com.getcapacitor.Logger;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/** The server the user picked, and the Capacitor configuration that loads it. */
|
||||
final class TrekServerStore {
|
||||
|
||||
private static final String PREFS = "trek_shell";
|
||||
private static final String KEY_URL = "server_url";
|
||||
private static final String CONFIG_DIR = "trek-server-config";
|
||||
|
||||
private TrekServerStore() {}
|
||||
|
||||
static String getUrl(Context context) {
|
||||
return prefs(context).getString(KEY_URL, null);
|
||||
}
|
||||
|
||||
static void setUrl(Context context, String url) {
|
||||
SharedPreferences.Editor editor = prefs(context).edit();
|
||||
if (url == null) {
|
||||
editor.remove(KEY_URL);
|
||||
} else {
|
||||
editor.putString(KEY_URL, url);
|
||||
}
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* The bundled capacitor.config.json with server.url set. Capacitor only
|
||||
* takes the server URL from a config file, so this writes a copy into the
|
||||
* app's private storage and loads that. Falls back to the bundled config,
|
||||
* which shows the address screen, if the copy cannot be written.
|
||||
*/
|
||||
static CapConfig configFor(Context context, String url) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(readAsset(context, "capacitor.config.json"));
|
||||
JSONObject server = json.optJSONObject("server");
|
||||
if (server == null) {
|
||||
server = new JSONObject();
|
||||
json.put("server", server);
|
||||
}
|
||||
server.put("url", url);
|
||||
|
||||
File dir = new File(context.getFilesDir(), CONFIG_DIR);
|
||||
if (!dir.isDirectory() && !dir.mkdirs()) throw new IOException("Cannot create " + dir);
|
||||
try (FileOutputStream out = new FileOutputStream(new File(dir, "capacitor.config.json"))) {
|
||||
out.write(json.toString().getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return CapConfig.loadFromFile(context, dir.getAbsolutePath());
|
||||
} catch (IOException | JSONException ex) {
|
||||
Logger.error("TrekShell", "Could not point the app at " + url, ex);
|
||||
return CapConfig.loadDefault(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static String readAsset(Context context, String name) throws IOException {
|
||||
try (InputStream in = context.getAssets().open(name); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) out.write(buffer, 0, read);
|
||||
return out.toString("UTF-8");
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedPreferences prefs(Context context) {
|
||||
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.liketrek.trek;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/** The native half of client/src/native/trekShell.ts. */
|
||||
@CapacitorPlugin(name = "TrekShell")
|
||||
public class TrekShellPlugin extends Plugin {
|
||||
|
||||
/** How long to wait after returning from the browser for the redirect to land. */
|
||||
private static final long CANCEL_GRACE_MS = 600;
|
||||
|
||||
private String authCallId;
|
||||
private String authScheme;
|
||||
private boolean authBrowserOpen;
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@PluginMethod
|
||||
public void getServer(PluginCall call) {
|
||||
String url = TrekServerStore.getUrl(getContext());
|
||||
JSObject result = new JSObject();
|
||||
result.put("url", url == null ? JSONObject.NULL : url);
|
||||
call.resolve(result);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void setServer(PluginCall call) {
|
||||
String url = call.getString("url");
|
||||
Uri parsed = url == null ? null : Uri.parse(url);
|
||||
if (parsed == null || parsed.getHost() == null || !("https".equals(parsed.getScheme()) || "http".equals(parsed.getScheme()))) {
|
||||
call.reject("A http(s) URL is required");
|
||||
return;
|
||||
}
|
||||
TrekServerStore.setUrl(getContext(), url);
|
||||
call.resolve();
|
||||
restart();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void resetServer(PluginCall call) {
|
||||
TrekServerStore.setUrl(getContext(), null);
|
||||
call.resolve();
|
||||
restart();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void reload(PluginCall call) {
|
||||
call.resolve();
|
||||
restart();
|
||||
}
|
||||
|
||||
/** The server URL is part of the bridge's configuration, so it takes a new activity. */
|
||||
private void restart() {
|
||||
getActivity().runOnUiThread(() -> getActivity().recreate());
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void authenticate(PluginCall call) {
|
||||
String url = call.getString("url");
|
||||
String scheme = call.getString("callbackScheme");
|
||||
if (url == null || scheme == null) {
|
||||
call.reject("url and callbackScheme are required");
|
||||
return;
|
||||
}
|
||||
rejectPendingAuth("Superseded by a new sign-in");
|
||||
call.setKeepAlive(true);
|
||||
getBridge().saveCall(call);
|
||||
authCallId = call.getCallbackId();
|
||||
authScheme = scheme;
|
||||
authBrowserOpen = true;
|
||||
// A Custom Tab shares the browser's cookies, so a running SSO session at
|
||||
// the identity provider carries over. The redirect back to the app's
|
||||
// scheme arrives through the intent-filter in AndroidManifest.xml.
|
||||
new CustomTabsIntent.Builder().build().launchUrl(getActivity(), Uri.parse(url));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleOnNewIntent(Intent intent) {
|
||||
super.handleOnNewIntent(intent);
|
||||
Uri data = intent.getData();
|
||||
PluginCall call = pendingAuthCall();
|
||||
if (call == null || data == null || !authScheme.equals(data.getScheme())) return;
|
||||
JSObject result = new JSObject();
|
||||
result.put("url", data.toString());
|
||||
call.resolve(result);
|
||||
finishAuth(call);
|
||||
}
|
||||
|
||||
/**
|
||||
* Back in the app without a redirect means the user closed the browser.
|
||||
* onNewIntent runs before onResume when the redirect did arrive, so the
|
||||
* grace period only covers devices that deliver the two out of order.
|
||||
*/
|
||||
@Override
|
||||
protected void handleOnResume() {
|
||||
super.handleOnResume();
|
||||
if (!authBrowserOpen) return;
|
||||
authBrowserOpen = false;
|
||||
handler.postDelayed(() -> {
|
||||
PluginCall call = pendingAuthCall();
|
||||
if (call == null) return;
|
||||
call.reject("Sign-in cancelled", "CANCELLED");
|
||||
finishAuth(call);
|
||||
}, CANCEL_GRACE_MS);
|
||||
}
|
||||
|
||||
private PluginCall pendingAuthCall() {
|
||||
return authCallId == null ? null : getBridge().getSavedCall(authCallId);
|
||||
}
|
||||
|
||||
private void rejectPendingAuth(String message) {
|
||||
PluginCall call = pendingAuthCall();
|
||||
if (call == null) return;
|
||||
call.reject(message, "CANCELLED");
|
||||
finishAuth(call);
|
||||
}
|
||||
|
||||
private void finishAuth(PluginCall call) {
|
||||
getBridge().releaseCall(call);
|
||||
authCallId = null;
|
||||
authScheme = null;
|
||||
authBrowserOpen = false;
|
||||
}
|
||||
|
||||
/** iOS only: Android keeps the service worker, which already starts TREK offline. */
|
||||
@PluginMethod
|
||||
public void refreshOfflineCopy(PluginCall call) {
|
||||
JSObject result = new JSObject();
|
||||
result.put("saved", false);
|
||||
call.resolve(result);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void openOfflineCopy(PluginCall call) {
|
||||
JSObject result = new JSObject();
|
||||
result.put("opened", false);
|
||||
call.resolve(result);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,4 @@
|
||||
<string name="title_activity_main">TREK - Plan your trips!</string>
|
||||
<string name="package_name">com.liketrek.trek</string>
|
||||
<string name="custom_url_scheme">com.liketrek.trek</string>
|
||||
<!-- Referenced by the FCM default_notification_channel_id meta-data in
|
||||
AndroidManifest.xml. A channel with this exact id must also be created
|
||||
at runtime via PushNotifications.createChannel(). -->
|
||||
<string name="default_notification_channel_id">trek_default</string>
|
||||
</resources>
|
||||
|
||||
@@ -5,45 +5,12 @@ project(':capacitor-android').projectDir = new File('../../node_modules/@capacit
|
||||
include ':capacitor-app'
|
||||
project(':capacitor-app').projectDir = new File('../../node_modules/@capacitor/app/android')
|
||||
|
||||
include ':capacitor-browser'
|
||||
project(':capacitor-browser').projectDir = new File('../../node_modules/@capacitor/browser/android')
|
||||
|
||||
include ':capacitor-calendar'
|
||||
project(':capacitor-calendar').projectDir = new File('../../node_modules/@capacitor/calendar/android')
|
||||
|
||||
include ':capacitor-camera'
|
||||
project(':capacitor-camera').projectDir = new File('../../node_modules/@capacitor/camera/android')
|
||||
|
||||
include ':capacitor-file-transfer'
|
||||
project(':capacitor-file-transfer').projectDir = new File('../../node_modules/@capacitor/file-transfer/android')
|
||||
|
||||
include ':capacitor-filesystem'
|
||||
project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capacitor/filesystem/android')
|
||||
|
||||
include ':capacitor-geolocation'
|
||||
project(':capacitor-geolocation').projectDir = new File('../../node_modules/@capacitor/geolocation/android')
|
||||
|
||||
include ':capacitor-haptics'
|
||||
project(':capacitor-haptics').projectDir = new File('../../node_modules/@capacitor/haptics/android')
|
||||
|
||||
include ':capacitor-inappbrowser'
|
||||
project(':capacitor-inappbrowser').projectDir = new File('../../node_modules/@capacitor/inappbrowser/android')
|
||||
|
||||
include ':capacitor-keyboard'
|
||||
project(':capacitor-keyboard').projectDir = new File('../../node_modules/@capacitor/keyboard/android')
|
||||
|
||||
include ':capacitor-local-notifications'
|
||||
project(':capacitor-local-notifications').projectDir = new File('../../node_modules/@capacitor/local-notifications/android')
|
||||
|
||||
include ':capacitor-network'
|
||||
project(':capacitor-network').projectDir = new File('../../node_modules/@capacitor/network/android')
|
||||
|
||||
include ':capacitor-preferences'
|
||||
project(':capacitor-preferences').projectDir = new File('../../node_modules/@capacitor/preferences/android')
|
||||
|
||||
include ':capacitor-push-notifications'
|
||||
project(':capacitor-push-notifications').projectDir = new File('../../node_modules/@capacitor/push-notifications/android')
|
||||
|
||||
include ':capacitor-share'
|
||||
project(':capacitor-share').projectDir = new File('../../node_modules/@capacitor/share/android')
|
||||
|
||||
@@ -52,6 +19,3 @@ project(':capacitor-splash-screen').projectDir = new File('../../node_modules/@c
|
||||
|
||||
include ':capacitor-status-bar'
|
||||
project(':capacitor-status-bar').projectDir = new File('../../node_modules/@capacitor/status-bar/android')
|
||||
|
||||
include ':capacitor-toast'
|
||||
project(':capacitor-toast').projectDir = new File('../../node_modules/@capacitor/toast/android')
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"_comment": [
|
||||
"Template only — this file is not read by the build.",
|
||||
"",
|
||||
"To enable Android push notifications:",
|
||||
" 1. Create a Firebase project at https://console.firebase.google.com",
|
||||
" 2. Add an Android app with package name com.liketrek.trek",
|
||||
" 3. Download the generated google-services.json",
|
||||
" 4. Place it at client/android/app/google-services.json (gitignored)",
|
||||
"",
|
||||
"Without it, app/build.gradle skips the google-services plugin and the app",
|
||||
"still builds and runs — PushNotifications.register() simply fails at runtime.",
|
||||
"",
|
||||
"The server that SENDS the notifications needs a service-account key from the",
|
||||
"SAME Firebase project. See docs for the self-hosting implications."
|
||||
],
|
||||
"project_info": {
|
||||
"project_number": "000000000000",
|
||||
"project_id": "your-firebase-project-id",
|
||||
"storage_bucket": "your-firebase-project-id.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:000000000000:android:0000000000000000000000",
|
||||
"android_client_info": {
|
||||
"package_name": "com.liketrek.trek"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "REPLACE_WITH_YOUR_ANDROID_API_KEY"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
ext {
|
||||
// 26, not Capacitor's default 24: @capacitor/inappbrowser requires a minimum
|
||||
// target of 26. This drops Android 7.0/7.1 (~1% of devices and no longer
|
||||
// receiving security updates). Removing @capacitor/inappbrowser — its system-
|
||||
// browser mode overlaps @capacitor/browser almost exactly — would allow 24
|
||||
// again.
|
||||
minSdkVersion = 26
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 36
|
||||
androidxActivityVersion = '1.11.0'
|
||||
@@ -18,7 +13,6 @@ ext {
|
||||
androidxJunitVersion = '1.3.0'
|
||||
androidxEspressoCoreVersion = '3.7.0'
|
||||
cordovaAndroidVersion = '14.0.1'
|
||||
// Consumed by @capacitor/push-notifications. Pinned rather than left to the
|
||||
// plugin's default so an FCM SDK bump is a visible, reviewable change here.
|
||||
firebaseMessagingVersion = '25.0.1'
|
||||
// Custom Tabs for SSO and passkey sign-in (TrekShellPlugin.authenticate).
|
||||
androidxBrowserVersion = '1.8.0'
|
||||
}
|
||||
Reference in New Issue
Block a user