Refresh entitlement after payment confirmation
This commit is contained in:
@@ -217,6 +217,13 @@ export function runTests() {
|
||||
hookSource.includes("retriedBackendJson"),
|
||||
"account snapshot loader must retry with a refreshed Supabase token when local user exists but /api/auth/me reports unauthenticated",
|
||||
);
|
||||
assert(
|
||||
hookSource.includes("refreshEntitlementAfterPayment") &&
|
||||
paymentFlowSource.includes("refreshEntitlementAfterPayment") &&
|
||||
paymentFlowSource.includes("await refreshEntitlementAfterPayment();") &&
|
||||
hookSource.includes("subscription_active === true"),
|
||||
"successful payment flows must automatically poll /api/auth/me until the paid subscription is visible instead of requiring logout or manual refresh",
|
||||
);
|
||||
assert(
|
||||
!hookSource.includes(".auth.getUser()") &&
|
||||
hookSource.includes(".auth.getSession()"),
|
||||
|
||||
@@ -146,10 +146,9 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
|
||||
[supabaseReady, getValidAccessToken],
|
||||
);
|
||||
|
||||
// ── loadSnapshot ──────────────────────────────────────────
|
||||
const loadSnapshot = useCallback(async () => {
|
||||
setErrorText("");
|
||||
const attempt = async (retry: boolean): Promise<void> => {
|
||||
// ── Auth snapshot refresh ────────────────────────────────
|
||||
const fetchAuthSnapshot = useCallback(
|
||||
async (retry: boolean): Promise<AuthMeResponse> => {
|
||||
const userPromise = supabaseReady
|
||||
? getSupabaseBrowserClient()
|
||||
.auth.getSession()
|
||||
@@ -165,7 +164,7 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
|
||||
if (!backendResult.ok) {
|
||||
if (retry && backendResult.status === 401) {
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
return attempt(false);
|
||||
return fetchAuthSnapshot(false);
|
||||
}
|
||||
const raw = (await backendResult.text()).slice(0, 260);
|
||||
throw new Error(copy.httpError.replace("{status}", String(backendResult.status)).replace("{raw}", raw));
|
||||
@@ -202,10 +201,41 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
|
||||
}
|
||||
setBackend(backendJson);
|
||||
setUpdatedAt(new Date().toISOString());
|
||||
};
|
||||
try { await attempt(true); }
|
||||
return backendJson;
|
||||
},
|
||||
[
|
||||
buildAuthedHeaders,
|
||||
copy.httpError,
|
||||
setBackend,
|
||||
setUpdatedAt,
|
||||
setUser,
|
||||
supabaseReady,
|
||||
],
|
||||
);
|
||||
|
||||
// ── loadSnapshot ──────────────────────────────────────────
|
||||
const loadSnapshot = useCallback(async () => {
|
||||
setErrorText("");
|
||||
try { await fetchAuthSnapshot(true); }
|
||||
catch (error) { setErrorText(String(error)); }
|
||||
}, [buildAuthedHeaders, supabaseReady, copy.httpError]);
|
||||
}, [fetchAuthSnapshot, setErrorText]);
|
||||
|
||||
const refreshEntitlementAfterPayment = useCallback(async () => {
|
||||
setErrorText("");
|
||||
const maxAttempts = 5;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const backendJson = await fetchAuthSnapshot(true);
|
||||
if (backendJson.subscription_active === true) return;
|
||||
} catch (error) {
|
||||
if (attempt === maxAttempts - 1) {
|
||||
setErrorText(String(error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
}
|
||||
}, [fetchAuthSnapshot, setErrorText]);
|
||||
|
||||
// ── Shared state for sub-hooks ───────────────────────────
|
||||
// These state variables are managed in the master hook and passed
|
||||
@@ -383,6 +413,7 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
|
||||
getValidAccessToken,
|
||||
buildAuthedHeaders,
|
||||
loadSnapshot,
|
||||
refreshEntitlementAfterPayment,
|
||||
loadPaymentSnapshot: loadPaymentSnapshotImpl,
|
||||
user,
|
||||
});
|
||||
@@ -438,6 +469,7 @@ export function useAccountPayment(params: UseAccountPaymentParams) {
|
||||
getValidAccessToken,
|
||||
buildAuthedHeaders,
|
||||
loadSnapshot,
|
||||
refreshEntitlementAfterPayment,
|
||||
loadPaymentSnapshot: loadPaymentSnapshotImpl,
|
||||
waitForReceipt: walletBind.waitForReceipt,
|
||||
ensureTargetChain: walletBind.ensureTargetChain,
|
||||
|
||||
@@ -53,6 +53,7 @@ export interface UseBillingParams {
|
||||
getValidAccessToken: () => Promise<string>;
|
||||
buildAuthedHeaders: (withJson?: boolean, requireAuth?: boolean) => Promise<Record<string, string>>;
|
||||
loadSnapshot: () => Promise<void>;
|
||||
refreshEntitlementAfterPayment: () => Promise<void>;
|
||||
loadPaymentSnapshot: () => Promise<void>;
|
||||
|
||||
// User for metadata points
|
||||
@@ -86,6 +87,7 @@ export function useBilling(params: UseBillingParams) {
|
||||
getValidAccessToken,
|
||||
buildAuthedHeaders,
|
||||
loadSnapshot,
|
||||
refreshEntitlementAfterPayment,
|
||||
loadPaymentSnapshot,
|
||||
user,
|
||||
} = params;
|
||||
@@ -203,7 +205,7 @@ export function useBilling(params: UseBillingParams) {
|
||||
if (json.ok) {
|
||||
setPaymentInfo(copy.walletRecoveryDone);
|
||||
setPaymentError("");
|
||||
await loadSnapshot();
|
||||
await refreshEntitlementAfterPayment();
|
||||
await loadPaymentSnapshot();
|
||||
return true;
|
||||
}
|
||||
@@ -215,7 +217,7 @@ export function useBilling(params: UseBillingParams) {
|
||||
}
|
||||
}, [
|
||||
authIsAuthenticated, buildAuthedHeaders, copy.walletRecoveryDone,
|
||||
loadPaymentSnapshot, loadSnapshot, reconcileBusy,
|
||||
loadPaymentSnapshot, refreshEntitlementAfterPayment, reconcileBusy,
|
||||
]);
|
||||
|
||||
// ── handleSubmit409 ────────────────────────────────────
|
||||
@@ -226,7 +228,7 @@ export function useBilling(params: UseBillingParams) {
|
||||
const ok = await reconcileLatestPayment();
|
||||
if (ok) return;
|
||||
setPaymentInfo(copy.orderAlreadyPaid);
|
||||
await loadSnapshot();
|
||||
await refreshEntitlementAfterPayment();
|
||||
await loadPaymentSnapshot();
|
||||
return;
|
||||
}
|
||||
@@ -246,7 +248,7 @@ export function useBilling(params: UseBillingParams) {
|
||||
const txHash = intentJson.intent?.tx_hash || txHashNorm;
|
||||
setPaymentInfo(`支付已确认,交易: ${shortAddress(txHash)}`);
|
||||
setPaymentError("");
|
||||
await loadSnapshot();
|
||||
await refreshEntitlementAfterPayment();
|
||||
await loadPaymentSnapshot();
|
||||
return;
|
||||
}
|
||||
@@ -257,7 +259,7 @@ export function useBilling(params: UseBillingParams) {
|
||||
}
|
||||
throw new Error(copy.submitTxFailed.replace("{raw}", raw));
|
||||
},
|
||||
[buildAuthedHeaders, copy, loadPaymentSnapshot, loadSnapshot, reconcileLatestPayment],
|
||||
[buildAuthedHeaders, copy, loadPaymentSnapshot, refreshEntitlementAfterPayment, reconcileLatestPayment],
|
||||
);
|
||||
|
||||
// ── openTelegramBotBindLink ──────────────────────────────
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface UsePaymentFlowParams {
|
||||
getValidAccessToken: () => Promise<string>;
|
||||
buildAuthedHeaders: (withJson?: boolean, requireAuth?: boolean) => Promise<Record<string, string>>;
|
||||
loadSnapshot: () => Promise<void>;
|
||||
refreshEntitlementAfterPayment: () => Promise<void>;
|
||||
loadPaymentSnapshot: () => Promise<void>;
|
||||
waitForReceipt: (txHash: string, provider?: EvmProvider, timeoutMs?: number, pollMs?: number) => Promise<any>;
|
||||
ensureTargetChain: (eth: EvmProvider, targetChainId: number, chain?: PaymentChainOption) => Promise<void>;
|
||||
@@ -160,6 +161,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
||||
getValidAccessToken,
|
||||
buildAuthedHeaders,
|
||||
loadSnapshot,
|
||||
refreshEntitlementAfterPayment,
|
||||
loadPaymentSnapshot,
|
||||
waitForReceipt,
|
||||
ensureTargetChain,
|
||||
@@ -349,7 +351,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
||||
intent_id: intentId,
|
||||
tx_hash: txHash || null,
|
||||
});
|
||||
await loadSnapshot();
|
||||
await refreshEntitlementAfterPayment();
|
||||
await loadPaymentSnapshot();
|
||||
return;
|
||||
}
|
||||
@@ -361,7 +363,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
||||
}
|
||||
throw new Error(copy.paymentPendingTimeout);
|
||||
},
|
||||
[loadPaymentSnapshot, loadSnapshot, selectedPlan?.plan_code],
|
||||
[loadPaymentSnapshot, refreshEntitlementAfterPayment, selectedPlan?.plan_code],
|
||||
);
|
||||
|
||||
// ── createIntentAndPay ──────────────────────────────────
|
||||
@@ -586,7 +588,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
||||
intent_id: intentId,
|
||||
tx_hash: txHashNorm,
|
||||
});
|
||||
await loadSnapshot();
|
||||
await refreshEntitlementAfterPayment();
|
||||
await loadPaymentSnapshot();
|
||||
} catch (error) {
|
||||
const normalized = normalizePaymentError(error);
|
||||
@@ -743,7 +745,7 @@ export function usePaymentFlow(params: UsePaymentFlowParams) {
|
||||
intent_id: intentIdVal,
|
||||
tx_hash: txHashNorm,
|
||||
});
|
||||
await loadSnapshot();
|
||||
await refreshEntitlementAfterPayment();
|
||||
await loadPaymentSnapshot();
|
||||
} catch (error) {
|
||||
setPaymentError(normalizePaymentError(error).message);
|
||||
|
||||
Reference in New Issue
Block a user