{"version":3,"file":"pending-checkout.mjs","names":[],"sources":["../../src/stripe/pending-checkout.ts"],"sourcesContent":["/**\n * Tracks checkouts that have been started but may not have been confirmed\n * yet — e.g. the user paid on Stripe and closed the tab before being\n * redirected back to the success page (where `confirmCheckoutSession` runs).\n *\n * IMPORTANT: this stores only the *session ids to retry confirmation on*, a\n * best-effort retry hint — NOT the user's paid/balance state. The server\n * remains the single source of truth for entitlements; on the next app boot\n * we simply re-run the (idempotent, ownership-checked) confirm for each\n * session so the credit balance reflects the purchase without waiting on the\n * async Stripe webhook. Scoped to a single browser; cross-device is covered by\n * the webhook backstop.\n *\n * A *list* (not a single value) is kept on purpose: localStorage is shared\n * across tabs, so two concurrent checkouts — or a second checkout started\n * before the first was reconciled — must not clobber each other.\n */\n\nconst KEY = 'pending-checkout';\n\n/** Drop a hint after this long — an abandoned/unpaid checkout shouldn't retry forever. */\nconst TTL = 60 * 60 * 1000; // 1 hour\n\n/** Cap the list so a string of abandoned checkouts can't grow it unbounded. */\nconst MAX = 5;\n\ntype Entry = { sessionId: string; ts: number };\n\nfunction isFresh(entry: Entry): boolean {\n  return Date.now() - entry.ts <= TTL;\n}\n\nfunction read(): Entry[] {\n  try {\n    const raw = localStorage.getItem(KEY);\n    if (!raw) return [];\n    const parsed = JSON.parse(raw);\n    if (!Array.isArray(parsed)) return [];\n    return parsed.filter(\n      (e): e is Entry => !!e && typeof e.sessionId === 'string' && typeof e.ts === 'number'\n    );\n  } catch {\n    return [];\n  }\n}\n\nfunction write(entries: Entry[]) {\n  try {\n    if (entries.length === 0) localStorage.removeItem(KEY);\n    else localStorage.setItem(KEY, JSON.stringify(entries.slice(-MAX)));\n  } catch {\n    // ignore — storage unavailable / quota; the webhook still reconciles.\n  }\n}\n\n/** Record a started checkout to retry confirmation on later. Dedupes by session id. */\nexport function addPendingCheckout(sessionId: string) {\n  const entries = read().filter((e) => isFresh(e) && e.sessionId !== sessionId);\n  entries.push({ sessionId, ts: Date.now() });\n  write(entries);\n}\n\n/** Fresh session ids awaiting confirmation. Persists the pruned list as a side effect. */\nexport function getPendingCheckouts(): string[] {\n  const all = read();\n  const fresh = all.filter(isFresh);\n  if (fresh.length !== all.length) write(fresh);\n  return fresh.map((e) => e.sessionId);\n}\n\n/** Drop one session once it's confirmed (or known not to belong to this user). */\nexport function removePendingCheckout(sessionId: string) {\n  write(read().filter((e) => isFresh(e) && e.sessionId !== sessionId));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkBA,MAAM,MAAM;;AAGZ,MAAM,MAAM,OAAU;AAOtB,SAAS,QAAQ,OAAuB;CACtC,OAAO,KAAK,IAAI,IAAI,MAAM,MAAM;AAClC;AAEA,SAAS,OAAgB;CACvB,IAAI;EACF,MAAM,MAAM,aAAa,QAAQ,GAAG;EACpC,IAAI,CAAC,KAAK,OAAO,CAAC;EAClB,MAAM,SAAS,KAAK,MAAM,GAAG;EAC7B,IAAI,CAAC,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC;EACpC,OAAO,OAAO,QACX,MAAkB,CAAC,CAAC,KAAK,OAAO,EAAE,cAAc,YAAY,OAAO,EAAE,OAAO,QAC/E;CACF,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAS,MAAM,SAAkB;CAC/B,IAAI;EACF,IAAI,QAAQ,WAAW,GAAG,aAAa,WAAW,GAAG;OAChD,aAAa,QAAQ,KAAK,KAAK,UAAU,QAAQ,MAAM,EAAI,CAAC,CAAC;CACpE,QAAQ,CAER;AACF;;AAGA,SAAgB,mBAAmB,WAAmB;CACpD,MAAM,UAAU,KAAK,CAAC,CAAC,QAAQ,MAAM,QAAQ,CAAC,KAAK,EAAE,cAAc,SAAS;CAC5E,QAAQ,KAAK;EAAE;EAAW,IAAI,KAAK,IAAI;CAAE,CAAC;CAC1C,MAAM,OAAO;AACf;;AAGA,SAAgB,sBAAgC;CAC9C,MAAM,MAAM,KAAK;CACjB,MAAM,QAAQ,IAAI,OAAO,OAAO;CAChC,IAAI,MAAM,WAAW,IAAI,QAAQ,MAAM,KAAK;CAC5C,OAAO,MAAM,KAAK,MAAM,EAAE,SAAS;AACrC;;AAGA,SAAgB,sBAAsB,WAAmB;CACvD,MAAM,KAAK,CAAC,CAAC,QAAQ,MAAM,QAAQ,CAAC,KAAK,EAAE,cAAc,SAAS,CAAC;AACrE"}