import { readLinkToken } from './credentialRecoveryLink'; // The emailed link is `/#/pay-invoice?t=`. The token is kept for this tab only, so the page // still knows it when the payment provider sends the payer back without it, and it is taken out of the // address bar as soon as it is read so it is not left in history or copied along with the page address. export const PAY_LINK_TOKEN_PARAM = 't'; export const PAY_LINK_TOKEN_STORAGE_KEY = 'akashic.invoice-pay-link.token'; export const PAY_LINK_PATH = '/pay-invoice'; export const PAY_LINK_RETURN_PARAM = 'return'; const readStored = (): string | undefined => { try { return window.sessionStorage.getItem(PAY_LINK_TOKEN_STORAGE_KEY) || undefined; } catch { return undefined; } }; const writeStored = (token: string): void => { try { window.sessionStorage.setItem(PAY_LINK_TOKEN_STORAGE_KEY, token); } catch { /* non-fatal: the link in the address still works for this visit. */ } }; const stripTokenFromAddress = (): void => { try { const { origin, pathname, search } = window.location; window.history.replaceState(window.history.state, '', `${origin}${pathname}${search}#${PAY_LINK_PATH}`); } catch { /* non-fatal. */ } }; /** The token for this visit: the link's own when it carries one, else the one this tab kept. */ export const resolvePayLinkToken = (href: string): string | undefined => { const fromLink = readLinkToken(PAY_LINK_TOKEN_PARAM, href); if (fromLink) { writeStored(fromLink); stripTokenFromAddress(); return fromLink; } return readStored(); }; /** Whether the payer has just come back from the payment page having paid. */ export const returnedFromPayment = (href: string): boolean => readLinkToken(PAY_LINK_RETURN_PARAM, href) === 'paid'; /** Where the payment page sends the payer back to; the token never leaves this tab. */ export const payLinkReturnUrls = (origin: string, pathname: string): { successUrl: string; cancelUrl: string } => { const base = `${origin}${pathname}#${PAY_LINK_PATH}`; return { successUrl: `${base}?${PAY_LINK_RETURN_PARAM}=paid`, cancelUrl: base }; }; // --- No-login operation envelope (F08) ---------------------------------------------------------------- // Kept in origin-local sessionStorage, keyed by a HASH of the pay-link token - never the raw token - so // a reopen/"Check again" on this same tab replays the same operation without ever writing the token // itself to storage. A deterministic, non-cryptographic hash is enough: it is a storage key, not a secret. // The envelope is written before the first send and carries the request exactly as sent. export interface PayLinkOperation { operationId: string; financialVersion: string; amountCents: number; currency: string; successUrl?: string; cancelUrl?: string; } /** The envelope cannot be read or kept: nothing may be sent, since a retry could not find what was. */ export class PayLinkStorageUnavailable extends Error {} const hashToken = (token: string): string => { let hash = 0; for (let i = 0; i < token.length; i += 1) { hash = (hash * 31 + token.charCodeAt(i)) | 0; } return (hash >>> 0).toString(36); }; const envelopeKey = (token: string): string => `akashic.invoice-pay-link.op.${hashToken(token)}`; const unavailable = (error: unknown) => new PayLinkStorageUnavailable(error instanceof Error ? error.message : String(error)); /** The stored envelope, or null when there is none. Unreadable storage or contents throw: never taken as absent. */ export const readPayLinkOperation = (token: string): PayLinkOperation | null => { let raw: string | null; try { raw = window.sessionStorage.getItem(envelopeKey(token)); } catch (error) { throw unavailable(error); } if (!raw) return null; try { const value = JSON.parse(raw) as Partial; if (typeof value.operationId === 'string' && typeof value.financialVersion === 'string') return value as PayLinkOperation; } catch { /* unreadable: reported below. */ } throw new PayLinkStorageUnavailable('The stored payment could not be read.'); }; export const writePayLinkOperation = (token: string, operation: PayLinkOperation | null): void => { try { if (operation) window.sessionStorage.setItem(envelopeKey(token), JSON.stringify(operation)); else window.sessionStorage.removeItem(envelopeKey(token)); } catch (error) { throw unavailable(error); } };