diff --git a/android-templates/MainActivity.java b/android-templates/MainActivity.java new file mode 100644 index 0000000..215f0d9 --- /dev/null +++ b/android-templates/MainActivity.java @@ -0,0 +1,101 @@ +package org.skatespots.app; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import com.getcapacitor.BridgeActivity; + +/** + * Custom MainActivity that lets the WebView hand OFF `intent:` / `nostrsigner:` + * URLs to the Android OS so Amber's approval screen actually opens. + * + * WHY THIS EXISTS + * ─────────────── + * When the JS signer navigates to an `intent:...;scheme=nostrsigner;...;end` + * URL, Capacitor's default WebView treats it as a page navigation and silently + * DROPS it — so Amber never launches and every signing action (delete, accept + * crew invite, zap, profile edit) hangs forever. Login works only because it + * goes through the native plugin's startActivityForResult; JS-initiated intents + * were never reaching the OS. On GrapheneOS this is enforced strictly. + * + * The fix: override shouldOverrideUrlLoading. Whenever the WebView tries to load + * an `intent:` or `nostrsigner:` URL, we parse it and call startActivity() + * ourselves — a REAL native launch that opens Amber, exactly like login. The + * result comes back via the app's custom-scheme deep link + * (org.skatespots.app://amber-callback...), handled by DeepLinkHandler / + * resolveAmberIntent in the web layer. + * + * This file is injected into the generated android/ project at build time by + * scripts/inject-main-activity.mjs (android/ is not committed). + */ +public class MainActivity extends BridgeActivity { + + private static final String TAG = "SkateSpotsAmber"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final WebView webView = getBridge().getWebView(); + if (webView == null) { + return; + } + + final WebViewClient existing = new WebViewClient(); + + webView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (handleExternalScheme(url)) { + return true; + } + return super.shouldOverrideUrlLoading(view, url); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, android.webkit.WebResourceRequest request) { + final String url = request.getUrl() != null ? request.getUrl().toString() : null; + if (url != null && handleExternalScheme(url)) { + return true; + } + return super.shouldOverrideUrlLoading(view, request); + } + }); + } + + /** + * If the URL is an Amber/signer intent, launch it natively and return true. + * Returns false for normal in-app URLs so Capacitor handles them as usual. + */ + private boolean handleExternalScheme(String url) { + if (url == null) { + return false; + } + + try { + if (url.startsWith("intent:")) { + // Parse the full Android intent: URI (carries scheme, extras, end). + Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } + + if (url.startsWith("nostrsigner:")) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + return true; + } + } catch (Exception e) { + Log.e(TAG, "Failed to launch external signer intent: " + e.getMessage(), e); + // Fall through — let the WebView try to handle it (will no-op safely). + } + + return false; + } +} diff --git a/package.json b/package.json index e796819..1090b39 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "skate-spots", "private": true, - "version": "1.2.7", + "version": "1.2.8", "type": "module", "homepage": "https://skate-spots.com", "scripts": { @@ -9,11 +9,11 @@ "build": "npm i --silent && vite build -l error && cp dist/index.html dist/404.html && echo 'Project built successfully!'", "test": "npm i --silent && tsc --noEmit && eslint && vitest run --reporter=dot --silent && vite build -l error && cp dist/index.html dist/404.html && echo 'All tests passed!'", "deploy": "npm i --silent && npm run build && npx -y nostr-deploy-cli deploy --skip-setup", - "android:init": "npm run android:build:apk && npx cap add android && npx cap sync android && node scripts/inject-amber-queries.mjs", - "android:sync": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs", + "android:init": "npm run android:build:apk && npx cap add android && npx cap sync android && node scripts/inject-amber-queries.mjs && node scripts/inject-main-activity.mjs", + "android:sync": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-main-activity.mjs", "android:open": "npx cap open android", "android:version": "node scripts/sync-android-version.mjs", - "android:build": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && cd android && ./gradlew assembleRelease", + "android:build": "npm run android:build:apk && npx cap sync android && node scripts/sync-android-version.mjs && node scripts/inject-amber-queries.mjs && node scripts/inject-main-activity.mjs && cd android && ./gradlew assembleRelease", "android:build:apk": "CAPACITOR_BUILD=1 vite build && sed -i 's|||g' dist/index.html && sed -i 's|