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:
parent
77b3c23633
commit
87f58cd84e
4 changed files with 95 additions and 1 deletions
|
|
@ -1537,6 +1537,14 @@ session with **no other memory of this project**. So:
|
||||||
auto-opened welcome modal. ⚠️ `SignupDialog` is a **default-only** export, so
|
auto-opened welcome modal. ⚠️ `SignupDialog` is a **default-only** export, so
|
||||||
lazy-load it as
|
lazy-load it as
|
||||||
`lazy(() => import('…/SignupDialog'))`.
|
`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
|
- **First-run checklist + backup-reminder gating (fixed 2026-10-12).** Two
|
||||||
onboarding prompts nagged returning users:
|
onboarding prompts nagged returning users:
|
||||||
1. `FirstRunChecklist` (map) showed for **any logged-in user** until they
|
1. `FirstRunChecklist` (map) showed for **any logged-in user** until they
|
||||||
|
|
|
||||||
78
src/components/ShareAppCard.tsx
Normal file
78
src/components/ShareAppCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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
|
* SkateSpots platform Lightning address. Receives Sasquatch Pro passes AND
|
||||||
* marketplace listing fees — the same address, deliberately. Single source of
|
* marketplace listing fees — the same address, deliberately. Single source of
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { useSeoMeta } from '@unhead/react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { ShareAppCard } from '@/components/ShareAppCard';
|
||||||
|
import { APP_INSTALL_URL } from '@/lib/platform';
|
||||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import {
|
import {
|
||||||
|
|
@ -233,7 +235,7 @@ Version: v0.1.0`;
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
href="https://zapstore.dev/apps/org.skatespots.app"
|
href={APP_INSTALL_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
|
|
@ -247,6 +249,9 @@ Version: v0.1.0`;
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Pass the app on (Android only) — QR / share sheet / NFC tag. */}
|
||||||
|
<ShareAppCard />
|
||||||
|
|
||||||
{/* Credits */}
|
{/* Credits */}
|
||||||
<Card className="bg-black/90 border-2 border-gray-700 mt-8">
|
<Card className="bg-black/90 border-2 border-gray-700 mt-8">
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue