mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Adds a shareable https://sauci.app/join/{code} link as an alternative to manually typing an 8-character invite code: - apps/web: /join/[code] page that validates the code shape client-side, attempts the app.sauci:// scheme link, and falls back to a large code display with copy button plus App Store/Play Store links; PostHog events for page view, code copy, and store-button clicks. - apps/web: apple-app-site-association and assetlinks.json under /.well-known so iOS Universal Links and Android App Links can verify sauci.app for /join/*. The Android release cert SHA-256 fingerprint is a placeholder pending the real value. - apps/mobile: associated domains (iOS) and an intent filter (Android) for https://sauci.app/join/*, alongside the existing app.sauci scheme. - apps/mobile: parse incoming join links, pre-fill and highlight the code on the pairing screen, stash the code for signed-out/mid-onboarding users and apply it once they reach pairing, and offer a once-per-session clipboard hand-off that is never auto-applied. - apps/mobile: share message now leads with the join link, keeping the raw code visible as a fallback. - Analytics: invite_link_opened, pairing_code_prefilled (mobile); join_page_viewed, join_page_code_copied, join_page_store_button_clicked (web). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
50 lines
1.8 KiB
TypeScript
50 lines
1.8 KiB
TypeScript
import { readFile } from 'node:fs/promises'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const componentPath = new URL('./JoinClient.tsx', import.meta.url)
|
|
|
|
describe('/join/[code] JoinClient', () => {
|
|
it('validates the code shape client-side without calling a private API', async () => {
|
|
const source = await readFile(componentPath, 'utf8')
|
|
|
|
expect(source).toContain("isValidInviteCode(code)")
|
|
expect(source).not.toMatch(/\/public\/v1\//)
|
|
expect(source).not.toMatch(/fetch\(/)
|
|
})
|
|
|
|
it('attempts the app scheme link and shows the raw code fallback', async () => {
|
|
const source = await readFile(componentPath, 'utf8')
|
|
|
|
expect(source).toContain('buildInviteSchemeUrl')
|
|
expect(source).toContain('invite-code-display')
|
|
expect(source).toContain('Copy code')
|
|
})
|
|
|
|
it('copies only the raw code, not a share message', async () => {
|
|
const source = await readFile(componentPath, 'utf8')
|
|
|
|
expect(source).toContain('navigator.clipboard.writeText(normalizedCode)')
|
|
})
|
|
|
|
it('links to both app stores', async () => {
|
|
const source = await readFile(componentPath, 'utf8')
|
|
|
|
expect(source).toContain('APP_STORE_URL')
|
|
expect(source).toContain('PLAY_STORE_URL')
|
|
})
|
|
|
|
it('instruments the funnel with PostHog page view, copy, and store-click events', async () => {
|
|
const source = await readFile(componentPath, 'utf8')
|
|
|
|
expect(source).toContain("posthog?.capture('join_page_viewed'")
|
|
expect(source).toContain("posthog?.capture('join_page_code_copied')")
|
|
expect(source).toContain("posthog?.capture('join_page_store_button_clicked'")
|
|
})
|
|
|
|
it('shows a clear fallback message for a malformed invite code', async () => {
|
|
const source = await readFile(componentPath, 'utf8')
|
|
|
|
expect(source).toContain('Invite link not recognized')
|
|
})
|
|
})
|