mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
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:
@@ -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">
|
||||
|
||||
@@ -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`;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user