Payment Flow resolved (#127)

* feat: implement payment success deep link handling and confirmation banner

- Added a new state to manage payment confirmation in BillingPanel.
- Implemented a deep link listener for payment success and cancellation events.
- Created functions to build payment success and cancel deep links.
- Displayed a confirmation banner upon successful payment, which auto-hides after 5 seconds.

* feat: enhance payment success handling in BillingPanel

- Updated the onPaymentSuccess function to be asynchronous.
- Added a call to fetch the current billing plan from the backend after a successful payment.
- Implemented error handling for the API call to ensure robustness.

* fix: improve payment success timeout handling in BillingPanel

- Introduced a timeoutRef to manage the auto-hide functionality of the payment confirmation banner.
- Ensured that the timeout is cleared when the component unmounts to prevent memory leaks.
This commit is contained in:
Mega Mind
2026-02-21 10:56:49 +04:00
committed by GitHub
parent 71aeb55f2e
commit 6356e3bdc4
3 changed files with 113 additions and 0 deletions
@@ -38,8 +38,10 @@ const BillingPanel = () => {
const [paymentMethod, setPaymentMethod] = useState<'card' | 'crypto'>('card');
const [isPurchasing, setIsPurchasing] = useState(false);
const [purchasingTier, setPurchasingTier] = useState<PlanTier | null>(null);
const [paymentConfirmed, setPaymentConfirmed] = useState(false);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const pollStartRef = useRef<number>(0);
const timeoutRef = useRef<number | null>(null);
// Fetch current plan on mount
useEffect(() => {
@@ -60,6 +62,40 @@ const BillingPanel = () => {
};
}, []);
// Handle payment:success deep link event
useEffect(() => {
const onPaymentSuccess = async () => {
// Stop any in-flight poll — we know checkout completed
if (pollRef.current) {
clearInterval(pollRef.current);
pollRef.current = null;
}
setIsPurchasing(false);
setPurchasingTier(null);
setPaymentConfirmed(true);
// Fetch current plan from backend, then refresh user/teams in store
try {
await billingApi.getCurrentPlan();
} catch (e) {
console.error('Failed to fetch current plan after payment', e);
}
dispatch(fetchCurrentUser());
// Auto-hide the success banner after 5 s
timeoutRef.current = window.setTimeout(() => setPaymentConfirmed(false), 5_000);
};
window.addEventListener('payment:success', onPaymentSuccess);
return () => {
window.removeEventListener('payment:success', onPaymentSuccess);
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
};
}, [dispatch]);
// ── Poll for plan change after checkout ─────────────────────────────
const currentTierRef = useRef(currentTier);
useEffect(() => {
@@ -295,6 +331,29 @@ const BillingPanel = () => {
})}
</div>
{/* ── Payment confirmed banner ─────────────────────────── */}
{paymentConfirmed && (
<div className="rounded-xl bg-sage-500/10 border border-sage-500/20 p-3 mx-4">
<div className="flex items-center gap-2">
<svg
className="w-4 h-4 text-sage-400 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<p className="text-xs text-sage-300 font-medium">
Payment confirmed! Your plan has been updated.
</p>
</div>
</div>
)}
{/* ── Purchasing overlay message ────────────────────────── */}
{isPurchasing && (
<div className="rounded-xl bg-amber-500/10 border border-amber-500/20 p-3 mx-4">
+9
View File
@@ -21,3 +21,12 @@ export const buildDesktopDeeplink = (token: string): string => {
const encoded = encodeURIComponent(token);
return `${DESKTOP_SCHEME}://auth?token=${encoded}`;
};
export const buildPaymentSuccessDeeplink = (sessionId: string): string => {
const encoded = encodeURIComponent(sessionId);
return `${DESKTOP_SCHEME}://payment/success?session_id=${encoded}`;
};
export const buildPaymentCancelDeeplink = (): string => {
return `${DESKTOP_SCHEME}://payment/cancel`;
};
+45
View File
@@ -29,6 +29,46 @@ const handleAuthDeepLink = async (parsed: URL) => {
window.location.hash = '/onboarding';
};
/**
* Handle `alphahuman://payment/success?session_id=...` deep links.
* Fired when a Stripe checkout session completes and the browser redirects
* back to the desktop app.
*/
const handlePaymentDeepLink = async (parsed: URL) => {
const path = parsed.pathname.replace(/^\/+/, '');
try {
await invoke('show_window');
} catch {
// Not fatal
}
if (path === 'success') {
const sessionId = parsed.searchParams.get('session_id');
if (!sessionId) {
console.warn('[DeepLink] Payment success missing session_id');
return;
}
console.log('[DeepLink] Payment success, session_id:', sessionId);
// Broadcast to the app so billing components can react
window.dispatchEvent(
new CustomEvent('payment:success', { detail: { sessionId } }),
);
// Navigate to billing settings to show confirmation
window.location.hash = '/settings/billing';
} else if (path === 'cancel') {
console.log('[DeepLink] Payment cancelled');
window.dispatchEvent(new CustomEvent('payment:cancel', {}));
window.location.hash = '/settings/billing';
} else {
console.warn('[DeepLink] Unknown payment path:', path);
}
};
/**
* Handle `alphahuman://oauth/success?integrationId=...&skillId=...`
* and `alphahuman://oauth/error?error=...&provider=...` deep links.
@@ -74,6 +114,8 @@ const handleOAuthDeepLink = async (parsed: URL) => {
* - `alphahuman://auth?token=...` → login flow
* - `alphahuman://oauth/success?...` → OAuth completion
* - `alphahuman://oauth/error?...` → OAuth failure
* - `alphahuman://payment/success?session_id=...` → Stripe payment confirmation
* - `alphahuman://payment/cancel` → Stripe payment cancellation
*/
const handleDeepLinkUrls = async (urls: string[] | null | undefined) => {
if (!urls || urls.length === 0) {
@@ -95,6 +137,9 @@ const handleDeepLinkUrls = async (urls: string[] | null | undefined) => {
case 'oauth':
await handleOAuthDeepLink(parsed);
break;
case 'payment':
await handlePaymentDeepLink(parsed);
break;
default:
console.warn('[DeepLink] Unknown deep link hostname:', parsed.hostname);
break;