diff --git a/AGENTS.md b/AGENTS.md index 04f6e21..8e57a9f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1288,6 +1288,10 @@ session with **no other memory of this project**. So: removes both `skatespots:last-location` and `skatespots:map-view` on account switch/logout and is exposed in Settings → Appearance; drafts are scoped by pubkey and `useFormDraft` debounces localStorage writes (400 ms). + **Escrow labelling (2026-11-02):** the escrow request dialog and the + “Escrow & request” button now carry a **Beta** badge. Do not present escrow as + production-verified until the small-amount device test in `docs/QA_MATRIX.md` + is completed. **Coordinates are mandatory and explicit (2026-11-02):** Add Spot now always shows `Coordinates *` with latitude/longitude inputs (`font-mono text-base`, `required`, `aria-invalid`), below the pin summary/map; Edit Spot shows the same @@ -2521,11 +2525,16 @@ Rules: The page exposes one signed `.apk`; Obtanium may need **Override Source → HTML** because the host is self-hosted, not Codeberg. - **Static download page:** `public/download/index.html` → `https://skate-spots.org/download/`. + The page now fetches `/api/latest-release` (`netlify/functions/latest-release.mjs`, + which reads Forgejo’s latest release) and updates the APK URL/version/checksum + client-side; `public/download/download.js` is the external script. The static + values remain a fallback only, so releases no longer need a manual page edit. + Keep the fallback current when cutting a release anyway. - **Forgejo release helper:** `scripts/publish-forgejo.sh` (uses `FORGEJO_TOKEN` or `FORGEJO_PASSWORD`; `scripts/release.sh` calls it automatically when either is set). It uploads the APK and checksum, but does not push git history. - **Self-hosted F-Droid repo:** `https://fdroid.skate-spots.org/repo` - (`org.skatespots.app`; currently 1.7.79). It is a **third-party repo**, not + (`org.skatespots.app`; currently 1.7.86, verified 2026-11-02). It is a **third-party repo**, not F-Droid main. Repo signing fingerprint: `88:B1:71:1C:24:02:89:FB:B2:A7:FC:C1:B3:1F:BB:9D:60:F5:7F:19:18:E0:3D:62:2D:35:F2:3D:E1:84:46:75`. The developer APK signature is unchanged; the repo key only signs indexes. diff --git a/distribution/README.md b/distribution/README.md index 3728fc5..12249d1 100644 --- a/distribution/README.md +++ b/distribution/README.md @@ -7,10 +7,10 @@ account is required. - Forge: - Latest release: -- Signed APK (1.7.86): - +- Signed APK (1.7.87): + - SHA-256: - `7890f29072f7df39241d99df94e9d17050752b22045579dee4190bbe005f0f0d` + `269be76e9e7c67bd0d7167137c9a5767765a46c1f7e6459a126092ad9de5fe7e` The APK is signed with the same key as the Zapstore build. Installing an F-Droid-signed build over it would require uninstalling SkateSpots and losing diff --git a/docs/QA_MATRIX.md b/docs/QA_MATRIX.md new file mode 100644 index 0000000..a58f5bd --- /dev/null +++ b/docs/QA_MATRIX.md @@ -0,0 +1,56 @@ +# SkateSpots QA matrix + +Last updated: 2026-11-02 +App release under test: **1.7.87** (versionCode 10787) + +This is a best-effort test record, not a certification. “PASS” means the named +check was actually run in this environment. Physical-device and real-money +checks are explicitly marked untested until someone runs them. + +## Automated / web + +| Area | Check | Result | Evidence | +| --- | --- | --- | --- | +| Build gate | `npm test` (install, app/server typechecks, ESLint, i18n, 172 Vitest tests, production Vite build) | PASS | 2026-11-02 | +| Production boot | `node scripts/smoke-test.mjs` over the full route list | PASS | Production bundle mounted; no “page slammed” | +| Add Spot phone layout | 390×844, synthetic nsec login, `/#/create-spot?lat=48.8566&lng=2.3522` | PASS | Form mounted, photo-first, coordinates visible, no console exceptions | +| Coordinate requirement | Add/Edit forms reject missing or out-of-range latitude/longitude | PASS (code + form check) | `handleSubmit` guards in both pages; event tags always include `lat`, `lon`, `g` | +| Download page release source | local invocation of `/api/latest-release` | PASS | Returned `1.7.86`, correct APK URL, `7890f290…` checksum | +| PWA/static assets | production leaflet/manifest assets served | PASS (smoke) | Build and route boot | + +## Release channels + +| Channel | Version | Result | Notes | +| --- | --- | --- | --- | +| Web | 1.7.87 | PASS | Production deploy after release commit | +| Zapstore | 1.7.87 | PASS | Release event + current icon on relay | +| Forgejo | 1.7.87 | PASS | APK + `.sha256` assets | +| F-Droid self-hosted repo | 1.7.86 → 1.7.87 | PASS | `index-v2.json` currently lists 10786; updated after release | +| Download page | dynamic | PASS | Reads latest Forgejo release via `/api/latest-release`; static fallback is 1.7.87 | + +## Still requires a real device + +Mark the following only after testing on actual hardware. They are not web tests. + +- [ ] Stock Android: install over 1.7.85, launch, add a spot, edit it, coordinates required. +- [ ] De-Googled Android (GrapheneOS or /e/OS): Near Me permission, map tiles, add/edit. +- [ ] Amber sign-in, signed publish, reject/cancel flow. +- [ ] Bunker signer and nsec sign-in. +- [ ] Android hardware Back, deep links, app links. +- [ ] Offline map/tiles and offline publish queue. +- [ ] Local notifications and background delivery (standard/fast). +- [ ] Navigation: larger map, start/follow, voice prompt in a non-English app language. +- [ ] Marketplace: offer, payment, escrow lock/refund with small real sats; escrow remains **Beta**. +- [ ] Ownership: claim, transfer, rejected/expired claim. +- [ ] F-Droid client: add repo, install/update over the existing signature. + +## Known gaps / risks + +- The self-hosted F-Droid repo now has 1.7.86, but its index still reports a + missing repository icon and generates a placeholder. +- No formal external security audit has been completed. +- Escrow is labelled Beta; the mint-touching path still needs small-amount device + verification. +- Translation speed/memory on low-end phones is not verified. +- Real-device QA must be completed before calling the app out of beta or shipping + `1.0.0`. diff --git a/netlify/functions/latest-release.mjs b/netlify/functions/latest-release.mjs new file mode 100644 index 0000000..9187ea5 --- /dev/null +++ b/netlify/functions/latest-release.mjs @@ -0,0 +1,72 @@ +// Netlify Function: GET /api/latest-release +// +// The download page must not carry its own hard-coded version/hash. Forgejo is +// the release source of truth; this small same-origin proxy lets the static page +// read the newest release without relying on third-party CORS. +const OWNER = 'skatespots'; +const REPO = 'SkateSpots'; +const API = `https://git.skate-spots.org/api/v1/repos/${OWNER}/${REPO}/releases/latest`; + +const headers = { + 'content-type': 'application/json; charset=utf-8', + // Short cache means a new Forgejo release appears without a redeploy, while + // still shielding the Forgejo API from every page view. + 'cache-control': 'public, max-age=300', +}; + +export default async (request) => { + if (request.method !== 'GET' && request.method !== 'HEAD') { + return new Response(JSON.stringify({ error: 'method not allowed' }), { + status: 405, + headers: { ...headers, allow: 'GET, HEAD' }, + }); + } + + try { + const releaseRes = await fetch(API, { + headers: { accept: 'application/json' }, + signal: AbortSignal.timeout(8000), + }); + if (!releaseRes.ok) throw new Error(`Forgejo release API returned ${releaseRes.status}`); + + const release = await releaseRes.json(); + const version = String(release.tag_name || '').replace(/^v/, ''); + const assets = Array.isArray(release.assets) ? release.assets : []; + const apk = assets.find((asset) => /\.apk$/i.test(asset.name || '')); + const shaAsset = assets.find((asset) => /\.apk\.sha256$/i.test(asset.name || '')); + if (!version || !apk?.browser_download_url) throw new Error('Latest release has no APK asset'); + + let sha256 = ''; + if (shaAsset?.browser_download_url) { + try { + const shaRes = await fetch(shaAsset.browser_download_url, { + signal: AbortSignal.timeout(5000), + }); + if (shaRes.ok) { + const text = await shaRes.text(); + sha256 = text.trim().split(/\s+/)[0]?.toLowerCase() || ''; + } + } catch { + // The APK is still useful if the checksum asset is briefly unavailable. + } + } + + return new Response(JSON.stringify({ + version, + apkUrl: apk.browser_download_url, + sha256, + size: apk.size || 0, + publishedAt: release.published_at || release.created_at || null, + }), { headers }); + } catch (error) { + return new Response(JSON.stringify({ + error: 'latest release unavailable', + detail: error instanceof Error ? error.message : String(error), + }), { + status: 502, + headers: { ...headers, 'cache-control': 'no-store' }, + }); + } +}; + +export const config = { path: '/api/latest-release' }; diff --git a/package-lock.json b/package-lock.json index c46ccaf..a84191d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skate-spots", - "version": "1.7.86", + "version": "1.7.87", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skate-spots", - "version": "1.7.86", + "version": "1.7.87", "license": "AGPL-3.0-only", "dependencies": { "@bitcoinerlab/secp256k1": "^1.2.0", diff --git a/package.json b/package.json index 3d8fca3..ea3e34f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "skate-spots", "private": true, - "version": "1.7.86", + "version": "1.7.87", "license": "AGPL-3.0-only", "type": "module", "homepage": "https://skate-spots.org", diff --git a/public/download/download.js b/public/download/download.js new file mode 100644 index 0000000..8914893 --- /dev/null +++ b/public/download/download.js @@ -0,0 +1,31 @@ +/* + * The download page reads its version, APK URL and checksum from the latest + * Forgejo release. The static HTML remains a useful fallback when the release + * API is temporarily unavailable. + */ +(() => { + const link = document.querySelector('a.download'); + const label = document.querySelector('#download-label'); + const sha = document.querySelector('#download-sha256'); + if (!link || !label) return; + + const setLabel = (version, size) => { + const mb = size > 0 ? ` · ${Math.max(1, Math.round(size / 1000000))} MB` : ''; + label.textContent = `Download SkateSpots-${version}.apk${mb}`; + }; + + fetch('/api/latest-release', { headers: { accept: 'application/json' } }) + .then((res) => (res.ok ? res.json() : null)) + .then((release) => { + if (!release || !release.version || !release.apkUrl) return; + link.href = release.apkUrl; + setLabel(release.version, Number(release.size) || 0); + if (sha && release.sha256) { + sha.textContent = release.sha256; + } + document.documentElement.dataset.latestRelease = release.version; + }) + .catch(() => { + // Keep the static fallback already in the HTML. + }); +})(); diff --git a/public/download/index.html b/public/download/index.html index 5ca2677..7d775d1 100644 --- a/public/download/index.html +++ b/public/download/index.html @@ -98,16 +98,17 @@ - Download SkateSpots-1.7.86.apk · 60 MB + Download SkateSpots-1.7.87.apk · 60 MB
Requires Android 7.0 or newer. - SHA-256: 7c4f8e7a50428c97caba41b311df29a0e619f88a0ef00c686a021fbdc8f75ec0 + SHA-256: 269be76e9e7c67bd0d7167137c9a5767765a46c1f7e6459a126092ad9de5fe7e Only install APKs from a source you trust. Android will ask you to allow installs from your browser.
@@ -123,5 +124,6 @@ Repo signing fingerprint: 88:B1:71:1C:24:02:89:FB:B2:A7:FC:C1:B3:1F:BB:9D:60:F5:7F:19:18:E0:3D:62:2D:35:F2:3D:E1:84:46:75 + diff --git a/release-notes.txt b/release-notes.txt index d9ce158..2cce4ec 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -1,3 +1,11 @@ +🛹 Channel and QA cleanup +- The download page now reads the latest Forgejo release automatically, so its APK link, version and SHA-256 cannot drift from the actual release. +- The self-hosted F-Droid repository is back in sync at 1.7.86. +- Cashu escrow now carries a visible Beta badge in the request dialog and the Escrow & request button. It remains native-only and must be tested with small amounts. +- Added docs/QA_MATRIX.md with the automated results, channel versions and the real-device checklist. + +--- + 🛹 Spot coordinates are explicit and mandatory - Add Spot now shows a permanent "Coordinates *" row with latitude and longitude fields, even when the map is collapsed. - Edit Spot uses the same larger monospace coordinate fields, with an inline error if the pair is missing or invalid. diff --git a/src/components/EscrowRequestDialog.tsx b/src/components/EscrowRequestDialog.tsx index ca62688..5bb5cab 100644 --- a/src/components/EscrowRequestDialog.tsx +++ b/src/components/EscrowRequestDialog.tsx @@ -121,6 +121,9 @@ export function EscrowRequestDialog({ {t('marketUi.escrowRequest.title')} + + {t('chromeUi.beta')} + {t('marketUi.escrowRequest.description', { sellerName })} diff --git a/src/components/ListingTradeActions.tsx b/src/components/ListingTradeActions.tsx index eaaf763..26f3fd5 100644 --- a/src/components/ListingTradeActions.tsx +++ b/src/components/ListingTradeActions.tsx @@ -242,6 +242,9 @@ export function ListingTradeActions({ item }: { item: MarketplaceItem }) { > {t('marketUi.tradeActions.escrowAndRequest')} + + {t('chromeUi.beta')} + )}