From 87f58cd84ed565393e5fc8f5241fd38dbe9f5f75 Mon Sep 17 00:00:00 2001
From: SkateSpots
Date: Mon, 26 Oct 2026 11:41:51 +0700
Subject: [PATCH] feat(android): pass-the-app card (QR + share sheet + NFC tag)
About page renders ShareAppCard on Android only: a QR of the Zapstore listing,
a share-sheet button for the install link, and NfcWriteButton to write it to a
tag. Single-sourced APP_INSTALL_URL in lib/platform.ts (also used by the About
download button). No peer-to-peer APK transfer (needs native Wi-Fi Direct/BLE +
PackageInstaller + device QA); Zapstore handles install/update.
---
AGENTS.md | 8 ++++
src/components/ShareAppCard.tsx | 78 +++++++++++++++++++++++++++++++++
src/lib/platform.ts | 3 ++
src/pages/AboutPage.tsx | 7 ++-
4 files changed, 95 insertions(+), 1 deletion(-)
create mode 100644 src/components/ShareAppCard.tsx
diff --git a/AGENTS.md b/AGENTS.md
index 256e0c2..11c7ac1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1537,6 +1537,14 @@ session with **no other memory of this project**. So:
auto-opened welcome modal. ⚠️ `SignupDialog` is a **default-only** export, so
lazy-load it as
`lazy(() => import('…/SignupDialog'))`.
+ - **Pass the app on — Android only (2026-10-26):** the About page renders
+ `ShareAppCard` when `getPlatform() === 'android'` (APK/Zapstore; hidden on
+ web/iOS): a **QR of the Zapstore listing** (`APP_INSTALL_URL` in
+ `lib/platform.ts`, single-sourced), a **share-sheet** button (Nearby
+ Share/SMS/etc.), and `NfcWriteButton` to write the install URL to an NFC tag.
+ ⚠️ Deliberately **no peer-to-peer APK transfer** — that needs native Wi‑Fi
+ Direct/BLE + a `PackageInstaller` flow and real-device QA; Zapstore handles
+ installs/updates. (Not visually verified on-device yet.)
- **First-run checklist + backup-reminder gating (fixed 2026-10-12).** Two
onboarding prompts nagged returning users:
1. `FirstRunChecklist` (map) showed for **any logged-in user** until they
diff --git a/src/components/ShareAppCard.tsx b/src/components/ShareAppCard.tsx
new file mode 100644
index 0000000..314b429
--- /dev/null
+++ b/src/components/ShareAppCard.tsx
@@ -0,0 +1,78 @@
+import { useState } from 'react';
+import { Check, Share2 } from 'lucide-react';
+import { Card, CardContent } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { QRCodeCanvas } from '@/components/ui/qrcode';
+import { NfcWriteButton } from '@/components/NfcWriteButton';
+import { useTranslation } from '@/contexts/language';
+import { useToast } from '@/hooks/useToast';
+import { APP_INSTALL_URL, getPlatform } from '@/lib/platform';
+
+/**
+ * "Pass the app on" — Android only (APK / Zapstore).
+ *
+ * The QR carries the **Zapstore listing** for this app; scanning it on another
+ * Android phone opens the store page and installs/updates via Zapstore. The
+ * share button hands the same link to Android's share sheet (Nearby Share,
+ * Bluetooth, messages…), and — where the device supports NFC writing — the link
+ * can be written to a tag for a genuine tap-to-open handoff.
+ *
+ * We deliberately do NOT move APK bytes peer-to-peer: that needs native
+ * Wi‑Fi Direct/BLE + a PackageInstaller flow and real-device QA, and Zapstore
+ * already handles installs/updates. Renders nothing off Android (iOS can't
+ * sideload; desktop has no use for it).
+ */
+export function ShareAppCard() {
+ const { t } = useTranslation();
+ const { toast } = useToast();
+ const [copied, setCopied] = useState(false);
+
+ if (getPlatform() !== 'android') return null;
+
+ const share = async () => {
+ try {
+ if (navigator.share) {
+ await navigator.share({
+ title: 'SkateSpots',
+ text: t('settingsUi.about.downloadAppDesc'),
+ url: APP_INSTALL_URL,
+ });
+ return;
+ }
+ await navigator.clipboard.writeText(APP_INSTALL_URL);
+ setCopied(true);
+ toast({ title: t('common.copied'), description: APP_INSTALL_URL });
+ window.setTimeout(() => setCopied(false), 1600);
+ } catch {
+ // Share sheet cancelled — nothing to report.
+ }
+ };
+
+ return (
+
+
+
+
+ {t('common.share')} SkateSpots
+
+
{t('settingsUi.about.downloadAppDesc')}
+
+
+
+
+
+
+
+
+ {/* Tap-to-open: write the install link to an NFC tag (no-op where unsupported). */}
+
+
+
+ );
+}
diff --git a/src/lib/platform.ts b/src/lib/platform.ts
index fe5afb3..8b28650 100644
--- a/src/lib/platform.ts
+++ b/src/lib/platform.ts
@@ -27,6 +27,9 @@ export function getPlatform(): string {
}
}
+/** Canonical Zapstore listing for this app (install + updates). Android only. */
+export const APP_INSTALL_URL = 'https://zapstore.dev/apps/org.skatespots.app';
+
/**
* SkateSpots platform Lightning address. Receives Sasquatch Pro passes AND
* marketplace listing fees — the same address, deliberately. Single source of
diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx
index dd57ee9..7bde56e 100644
--- a/src/pages/AboutPage.tsx
+++ b/src/pages/AboutPage.tsx
@@ -2,6 +2,8 @@ import { useSeoMeta } from '@unhead/react';
import { Link } from 'react-router-dom';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
+import { ShareAppCard } from '@/components/ShareAppCard';
+import { APP_INSTALL_URL } from '@/lib/platform';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useToast } from '@/hooks/useToast';
import {
@@ -233,7 +235,7 @@ Version: v0.1.0`;