Files
Sauci/apps/mobile/src/__tests__/lib/inviteLink.test.ts
T
jjbruntonandClaude Fable 5 d48b44baa8 feat(invite-funnel): add web join page and mobile deep-link hand-off
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>
2026-08-30 17:58:43 +01:00

70 lines
2.5 KiB
TypeScript

import { isValidInviteCode, normalizeInviteCode, parseInviteLinkCode } from "../../lib/inviteLink";
describe("isValidInviteCode", () => {
it("accepts 8-character alphanumeric codes case-insensitively", () => {
expect(isValidInviteCode("ABCD1234")).toBe(true);
expect(isValidInviteCode("abcd1234")).toBe(true);
});
it("rejects codes with the wrong length or characters", () => {
expect(isValidInviteCode("ABC123")).toBe(false); // too short
expect(isValidInviteCode("ABCD12345")).toBe(false); // too long
expect(isValidInviteCode("ABCD-123")).toBe(false); // invalid character
expect(isValidInviteCode("")).toBe(false);
expect(isValidInviteCode(null)).toBe(false);
expect(isValidInviteCode(undefined)).toBe(false);
});
});
describe("normalizeInviteCode", () => {
it("trims and uppercases", () => {
expect(normalizeInviteCode(" abcd1234 ")).toBe("ABCD1234");
});
});
describe("parseInviteLinkCode", () => {
it("parses a universal link", () => {
expect(parseInviteLinkCode("https://sauci.app/join/abcd1234")).toEqual({
code: "ABCD1234",
source: "universal_link",
});
});
it("parses a universal link with a trailing slash", () => {
expect(parseInviteLinkCode("https://sauci.app/join/abcd1234/")).toEqual({
code: "ABCD1234",
source: "universal_link",
});
});
it("parses a custom scheme link", () => {
expect(parseInviteLinkCode("app.sauci://join?code=abcd1234")).toEqual({
code: "ABCD1234",
source: "scheme",
});
});
it("returns null for an unrelated https URL", () => {
expect(parseInviteLinkCode("https://sauci.app/redeem")).toBeNull();
});
it("returns null for an unrelated host", () => {
expect(parseInviteLinkCode("https://example.com/join/abcd1234")).toBeNull();
});
it("returns null for a custom scheme link with a badly-shaped code", () => {
expect(parseInviteLinkCode("app.sauci://join?code=123")).toBeNull();
});
it("returns null for a custom scheme link missing the join host", () => {
expect(parseInviteLinkCode("app.sauci://login?token=abc")).toBeNull();
});
it("returns null for garbage input", () => {
expect(parseInviteLinkCode("not a url")).toBeNull();
expect(parseInviteLinkCode(null)).toBeNull();
expect(parseInviteLinkCode(undefined)).toBeNull();
expect(parseInviteLinkCode("")).toBeNull();
});
});