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