All files / features/auth hydrate.ts

100% Statements 30/30
91.17% Branches 31/34
100% Functions 4/4
100% Lines 20/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75                2x                                               2x               4x 6x 3x 3x 3x 3x         3x 2x 2x 2x 2x 2x 2x 2x 2x 2x         3x 3x                  
import type { OnboardingData, RelationshipStatus } from "@/features/onboarding/useOnboarding";
import type { ProfileInput } from "@/lib/auth/callables";
 
/**
 * Pure mappers from the server-owned Firestore documents to the local zustand
 * stores. The auth bootstrap is the single writer of these fields, so all the
 * shape/derivation logic lives here where it can be unit-tested without native SDKs.
 */
const DAY_MS = 86_400_000;
 
export type CoupleDoc = {
  members: string[];
  profiles?: Record<string, { displayName?: string | null }>;
  togetherSince?: { toMillis: () => number } | null;
};
 
export type UserDoc = Partial<{
  displayName: string;
  gender: string | null;
  age: number;
  relationship: RelationshipStatus;
  anniversary: string | null;
  notificationsEnabled: boolean;
  completed: boolean;
}>;
 
export type PairingSnapshot = {
  isPaired: boolean;
  partnerName: string | null;
  togetherDays: number;
};
 
export const UNPAIRED: PairingSnapshot = { isPaired: false, partnerName: null, togetherDays: 0 };
 
/** Derive the pairing store from the couple doc (the day of pairing counts as day 1). */
export function pairingFromCouple(
  couple: CoupleDoc | null,
  uid: string,
  nowMs: number,
): PairingSnapshot {
  if (!couple) return UNPAIRED;
  const partnerId = couple.members.find((m) => m !== uid) ?? null;
  const partnerName = partnerId ? (couple.profiles?.[partnerId]?.displayName ?? null) : null;
  const since = couple.togetherSince?.toMillis?.() ?? nowMs;
  const togetherDays = Math.max(1, Math.floor((nowMs - since) / DAY_MS) + 1);
  return { isPaired: true, partnerName, togetherDays };
}
 
/** Derive the onboarding store patch from the user doc (only fields that are present). */
export function onboardingFromUser(user: UserDoc | null): Partial<OnboardingData> {
  if (!user) return {};
  const out: Partial<OnboardingData> = {};
  if (user.displayName !== undefined) out.displayName = user.displayName;
  if (user.gender !== undefined) out.gender = user.gender;
  if (user.age !== undefined) out.age = String(user.age);
  if (user.relationship !== undefined) out.relationship = user.relationship;
  if (user.anniversary !== undefined) out.anniversary = user.anniversary;
  if (user.notificationsEnabled !== undefined) out.notificationsEnabled = user.notificationsEnabled;
  Eif (user.completed !== undefined) out.completed = user.completed;
  return out;
}
 
/** Map the local onboarding answers to the `updateProfile` payload (age → number). */
export function profileInputFromOnboarding(data: OnboardingData): ProfileInput {
  const age = Number(data.age);
  return {
    displayName: data.displayName || undefined,
    gender: data.gender,
    age: Number.isFinite(age) && data.age !== "" ? age : undefined,
    relationship: data.relationship ?? undefined,
    anniversary: data.anniversary,
    notificationsEnabled: data.notificationsEnabled,
  };
}