Refresh entitlement after payment confirmation

This commit is contained in:
2569718930@qq.com
2026-05-30 16:26:32 +08:00
parent 91896b4dad
commit a03b8095ee
4 changed files with 60 additions and 17 deletions
@@ -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,
+7 -5
View File
@@ -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);