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.
This commit is contained in:
SkateSpots 2026-10-26 11:41:51 +07:00
parent 77b3c23633
commit 87f58cd84e
4 changed files with 95 additions and 1 deletions

View file

@ -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

View file

@ -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 (
<Card className="bg-black/90 border border-white/10 mt-4">
<CardContent className="space-y-3 p-5">
<div className="space-y-1 text-center">
<p className="text-xs font-black uppercase tracking-widest text-gray-300">
{t('common.share')} SkateSpots
</p>
<p className="text-xs text-gray-500">{t('settingsUi.about.downloadAppDesc')}</p>
</div>
<div className="mx-auto w-fit rounded-xl bg-white p-3">
<QRCodeCanvas value={APP_INSTALL_URL} size={180} level="M" />
</div>
<Button
className="w-full gap-2 bg-[#ccff00] font-bold text-black hover:bg-[#ccff00]/90"
onClick={() => void share()}
>
{copied ? <Check className="h-4 w-4" /> : <Share2 className="h-4 w-4" />}
{copied ? t('common.copied') : t('common.share')}
</Button>
{/* Tap-to-open: write the install link to an NFC tag (no-op where unsupported). */}
<NfcWriteButton url={APP_INSTALL_URL} />
</CardContent>
</Card>
);
}

View file

@ -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

View file

@ -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`;
</p>
</div>
<a
href="https://zapstore.dev/apps/org.skatespots.app"
href={APP_INSTALL_URL}
target="_blank"
rel="noopener noreferrer"
className="flex-shrink-0"
@ -247,6 +249,9 @@ Version: v0.1.0`;
</CardContent>
</Card>
{/* Pass the app on (Android only) — QR / share sheet / NFC tag. */}
<ShareAppCard />
{/* Credits */}
<Card className="bg-black/90 border-2 border-gray-700 mt-8">
<CardContent className="p-6">