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|