diff --git a/src/app/_components/core/index.ts b/src/app/_components/core/index.ts index bc1cc9bd..31e9c8e5 100644 --- a/src/app/_components/core/index.ts +++ b/src/app/_components/core/index.ts @@ -9,6 +9,7 @@ export * from "./dialog"; export * from "./fixed-bottom-area"; export * from "./loading-indicator"; export * from "./mobile-shell"; +export * from "./page-loading-fallback"; export * from "./page-scaffold"; export * from "./responsive-mobile-shell"; export * from "./scrollable-page"; diff --git a/src/app/_components/core/page-loading-fallback.module.css b/src/app/_components/core/page-loading-fallback.module.css new file mode 100644 index 00000000..f4bc0f81 --- /dev/null +++ b/src/app/_components/core/page-loading-fallback.module.css @@ -0,0 +1,19 @@ +.content { + display: grid; + min-height: 60vh; + place-items: center; + padding: 2rem; + text-align: center; +} + +.neutral { + color: #666; +} + +.dark { + color: rgba(255, 255, 255, 0.72); +} + +.warm { + color: #8b6265; +} diff --git a/src/app/_components/core/page-loading-fallback.tsx b/src/app/_components/core/page-loading-fallback.tsx new file mode 100644 index 00000000..e6b453fb --- /dev/null +++ b/src/app/_components/core/page-loading-fallback.tsx @@ -0,0 +1,20 @@ +import { MobileShell } from "./mobile-shell"; +import styles from "./page-loading-fallback.module.css"; + +export interface PageLoadingFallbackProps { + children: string; + background?: string; + tone?: "neutral" | "dark" | "warm"; +} + +export function PageLoadingFallback({ + children, + background, + tone = "neutral", +}: PageLoadingFallbackProps) { + return ( + + {children} + + ); +} diff --git a/src/app/_hooks/__tests__/use-payment-order-lifecycle.test.ts b/src/app/_hooks/__tests__/use-payment-order-lifecycle.test.ts new file mode 100644 index 00000000..e7b53cef --- /dev/null +++ b/src/app/_hooks/__tests__/use-payment-order-lifecycle.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import { shouldInspectPendingPaymentOrder } from "../use-payment-order-lifecycle"; + +describe("shouldInspectPendingPaymentOrder", () => { + it("allows pending order inspection when payment plans are ready", () => { + expect( + shouldInspectPendingPaymentOrder({ + currentOrderId: null, + isPaid: false, + isPollingOrder: false, + shouldResumePendingOrder: true, + status: "ready", + }), + ).toBe(true); + }); + + it("allows cleanup inspection for active stale payment state", () => { + expect( + shouldInspectPendingPaymentOrder({ + currentOrderId: "order-1", + isPaid: false, + isPollingOrder: true, + shouldResumePendingOrder: false, + status: "pollingOrder", + }), + ).toBe(true); + }); + + it("waits when resume is requested but payment plans are not ready", () => { + expect( + shouldInspectPendingPaymentOrder({ + currentOrderId: "order-1", + isPaid: false, + isPollingOrder: true, + shouldResumePendingOrder: true, + status: "pollingOrder", + }), + ).toBe(false); + }); +}); diff --git a/src/app/_hooks/use-payment-order-lifecycle.ts b/src/app/_hooks/use-payment-order-lifecycle.ts new file mode 100644 index 00000000..743f3f38 --- /dev/null +++ b/src/app/_hooks/use-payment-order-lifecycle.ts @@ -0,0 +1,136 @@ +"use client"; + +import { type Dispatch, useEffect, useRef } from "react"; + +import { + clearPendingPaymentOrder, + getPendingPaymentOrderForType, + type PendingPaymentSubscriptionType, +} from "@/lib/payment/pending_payment_order"; +import type { + PaymentContextState, +} from "@/stores/payment/payment-context"; +import type { PaymentEvent } from "@/stores/payment/payment-events"; +import { useUserDispatch } from "@/stores/user/user-context"; + +export interface PaymentOrderLifecycleState { + currentOrderId: string | null; + isPaid: boolean; + isPollingOrder: boolean; + status: string; +} + +export interface ShouldInspectPendingPaymentOrderInput + extends PaymentOrderLifecycleState { + shouldResumePendingOrder: boolean; +} + +export interface UsePaymentOrderLifecycleInput { + payment: PaymentContextState; + paymentDispatch: Dispatch; + paymentType: PendingPaymentSubscriptionType; + shouldResumePendingOrder: boolean; + refreshUserOnPaid?: boolean; +} + +export function shouldInspectPendingPaymentOrder({ + isPaid, + isPollingOrder, + shouldResumePendingOrder, + status, +}: ShouldInspectPendingPaymentOrderInput): boolean { + return ( + status === "ready" || + (!shouldResumePendingOrder && + (isPollingOrder || isPaid || status === "failed")) + ); +} + +export function usePaymentOrderLifecycle({ + payment, + paymentDispatch, + paymentType, + refreshUserOnPaid = true, + shouldResumePendingOrder, +}: UsePaymentOrderLifecycleInput): void { + const userDispatch = useUserDispatch(); + const refreshedPaidOrderRef = useRef(null); + const resumedPendingOrderRef = useRef(null); + + useEffect(() => { + if (!refreshUserOnPaid) return; + if (!payment.isPaid || !payment.currentOrderId) return; + if (refreshedPaidOrderRef.current === payment.currentOrderId) return; + refreshedPaidOrderRef.current = payment.currentOrderId; + userDispatch({ type: "UserFetch" }); + }, [ + payment.currentOrderId, + payment.isPaid, + refreshUserOnPaid, + userDispatch, + ]); + + useEffect(() => { + if ( + !shouldInspectPendingPaymentOrder({ + currentOrderId: payment.currentOrderId, + isPaid: payment.isPaid, + isPollingOrder: payment.isPollingOrder, + shouldResumePendingOrder, + status: payment.status, + }) + ) { + return; + } + + let cancelled = false; + + const handlePendingOrder = async () => { + const result = await getPendingPaymentOrderForType(paymentType); + if (cancelled || !result.success || result.data === null) return; + + if (!shouldResumePendingOrder) { + await clearPendingPaymentOrder(); + if ( + payment.currentOrderId === result.data.orderId && + (payment.isPollingOrder || + payment.isPaid || + payment.status === "failed") + ) { + paymentDispatch({ type: "PaymentReset" }); + } + return; + } + + if (payment.currentOrderId === result.data.orderId) return; + if (resumedPendingOrderRef.current === result.data.orderId) return; + + resumedPendingOrderRef.current = result.data.orderId; + paymentDispatch({ + type: "PaymentReturned", + orderId: result.data.orderId, + createdAt: result.data.createdAt, + }); + }; + + void handlePendingOrder(); + return () => { + cancelled = true; + }; + }, [ + payment.currentOrderId, + payment.isPaid, + payment.isPollingOrder, + payment.status, + paymentDispatch, + paymentType, + shouldResumePendingOrder, + ]); + + useEffect(() => { + if (!payment.currentOrderId) return; + if (!payment.isPaid && payment.status !== "failed") return; + + void clearPendingPaymentOrder(); + }, [payment.currentOrderId, payment.isPaid, payment.status]); +} diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 3b83af20..435382ee 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -1,8 +1,6 @@ -"use client"; - import { Suspense } from "react"; -import { MobileShell } from "@/app/_components/core"; +import { PageLoadingFallback } from "@/app/_components/core"; import { ChatScreen } from "@/app/chat/chat-screen"; export default function ChatPage() { @@ -15,17 +13,8 @@ export default function ChatPage() { function ChatFallback() { return ( - - - Loading chat... - - + + Loading chat... + ); } diff --git a/src/app/private-room/__tests__/use-private-room-flow.test.ts b/src/app/private-room/__tests__/use-private-room-flow.test.ts new file mode 100644 index 00000000..beeeb9d5 --- /dev/null +++ b/src/app/private-room/__tests__/use-private-room-flow.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; + +import { isPrivateRoomAuthRequired } from "../use-private-room-flow"; + +describe("isPrivateRoomAuthRequired", () => { + it.each(["guest", "notLoggedIn"] as const)( + "requires auth for %s users", + (loginStatus) => { + expect(isPrivateRoomAuthRequired(loginStatus)).toBe(true); + }, + ); + + it.each(["email", "facebook", "google", "apple"] as const)( + "allows %s users to top up directly", + (loginStatus) => { + expect(isPrivateRoomAuthRequired(loginStatus)).toBe(false); + }, + ); +}); diff --git a/src/app/private-room/private-room-screen.tsx b/src/app/private-room/private-room-screen.tsx index 31caa010..1acc7ac0 100644 --- a/src/app/private-room/private-room-screen.tsx +++ b/src/app/private-room/private-room-screen.tsx @@ -1,7 +1,7 @@ "use client"; import type { CSSProperties } from "react"; -import { useEffect, useMemo, useRef } from "react"; +import { useMemo } from "react"; import Image from "next/image"; import { ImageIcon, @@ -12,7 +12,6 @@ import { import type { PrivateRoomMoment } from "@/data/dto/private-room"; import { AppBottomNav, MobileShell } from "@/app/_components/core"; -import { useGuestLoginBootstrap } from "@/hooks/use-guest-login-bootstrap"; import { ROUTES } from "@/router/routes"; import { useAppNavigator } from "@/router/use-app-navigator"; import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context"; @@ -20,9 +19,14 @@ import { usePrivateRoomDispatch, usePrivateRoomState, } from "@/stores/private-room"; -import { useUserDispatch } from "@/stores/user/user-context"; import styles from "./private-room-screen.module.css"; +import { + isPrivateRoomAuthRequired, + usePrivateRoomBootstrapFlow, + usePrivateRoomUnlockPaywallNavigation, + usePrivateRoomUnlockSuccessRefresh, +} from "./use-private-room-flow"; const FALLBACK_PROFILE = { name: "Elio Silvestri", @@ -37,70 +41,23 @@ export function PrivateRoomScreen() { const navigator = useAppNavigator(); const authState = useAuthState(); const authDispatch = useAuthDispatch(); - const userDispatch = useUserDispatch(); const state = usePrivateRoomState(); const dispatch = usePrivateRoomDispatch(); - const previousLoginStatusRef = useRef(authState.loginStatus); - const lastUnlockSuccessNonceRef = useRef(0); - useGuestLoginBootstrap({ - dispatch: authDispatch, + usePrivateRoomBootstrapFlow({ + authDispatch, hasInitialized: authState.hasInitialized, - isLoading: authState.isLoading, + isAuthLoading: authState.isLoading, loginStatus: authState.loginStatus, + roomDispatch: dispatch, + roomStatus: state.status, }); - - useEffect(() => { - if (!authState.hasInitialized || authState.isLoading) return; - if (authState.loginStatus === "notLoggedIn") return; - - const previousLoginStatus = previousLoginStatusRef.current; - previousLoginStatusRef.current = authState.loginStatus; - - if (state.status === "idle") { - dispatch({ type: "PrivateRoomInit" }); - return; - } - - if ( - previousLoginStatus !== authState.loginStatus && - state.status !== "loading" - ) { - dispatch({ type: "PrivateRoomRefresh" }); - } - }, [ - authState.hasInitialized, - authState.isLoading, - authState.loginStatus, - dispatch, - state.status, - ]); - - useEffect(() => { - const request = state.unlockPaywallRequest; - if (!request) return; - - if ( - authState.loginStatus === "guest" || - authState.loginStatus === "notLoggedIn" - ) { - navigator.openAuth(ROUTES.privateRoom); - } else { - navigator.openSubscription({ type: "topup" }); - } - dispatch({ type: "PrivateRoomUnlockPaywallConsumed" }); - }, [ - authState.loginStatus, - dispatch, - navigator, - state.unlockPaywallRequest, - ]); - - useEffect(() => { - if (state.unlockSuccessNonce <= lastUnlockSuccessNonceRef.current) return; - lastUnlockSuccessNonceRef.current = state.unlockSuccessNonce; - userDispatch({ type: "UserFetch" }); - }, [state.unlockSuccessNonce, userDispatch]); + usePrivateRoomUnlockPaywallNavigation({ + loginStatus: authState.loginStatus, + roomDispatch: dispatch, + unlockPaywallRequest: state.unlockPaywallRequest, + }); + usePrivateRoomUnlockSuccessRefresh(state.unlockSuccessNonce); const profile = state.profile; const displayName = @@ -117,10 +74,7 @@ export function PrivateRoomScreen() { ); const handleTopUpClick = () => { - if ( - authState.loginStatus === "guest" || - authState.loginStatus === "notLoggedIn" - ) { + if (isPrivateRoomAuthRequired(authState.loginStatus)) { navigator.openAuth(ROUTES.privateRoom); return; } diff --git a/src/app/private-room/use-private-room-flow.ts b/src/app/private-room/use-private-room-flow.ts new file mode 100644 index 00000000..cb7ca57a --- /dev/null +++ b/src/app/private-room/use-private-room-flow.ts @@ -0,0 +1,104 @@ +"use client"; + +import { type Dispatch, useEffect, useRef } from "react"; + +import type { LoginStatus } from "@/data/dto/auth"; +import { useGuestLoginBootstrap } from "@/hooks/use-guest-login-bootstrap"; +import { ROUTES } from "@/router/routes"; +import { useAppNavigator } from "@/router/use-app-navigator"; +import type { AuthEvent } from "@/stores/auth/auth-events"; +import type { PrivateRoomEvent } from "@/stores/private-room"; +import type { PrivateRoomUnlockPaywallRequest } from "@/stores/private-room/private-room-state"; +import { useUserDispatch } from "@/stores/user/user-context"; + +export interface UsePrivateRoomBootstrapFlowInput { + authDispatch: Dispatch; + hasInitialized: boolean; + isAuthLoading: boolean; + loginStatus: LoginStatus; + roomDispatch: Dispatch; + roomStatus: string; +} + +export interface UsePrivateRoomUnlockPaywallNavigationInput { + loginStatus: LoginStatus; + roomDispatch: Dispatch; + unlockPaywallRequest: PrivateRoomUnlockPaywallRequest | null; +} + +export function isPrivateRoomAuthRequired(loginStatus: LoginStatus): boolean { + return loginStatus === "guest" || loginStatus === "notLoggedIn"; +} + +export function usePrivateRoomBootstrapFlow({ + authDispatch, + hasInitialized, + isAuthLoading, + loginStatus, + roomDispatch, + roomStatus, +}: UsePrivateRoomBootstrapFlowInput): void { + const previousLoginStatusRef = useRef(loginStatus); + + useGuestLoginBootstrap({ + dispatch: authDispatch, + hasInitialized, + isLoading: isAuthLoading, + loginStatus, + }); + + useEffect(() => { + if (!hasInitialized || isAuthLoading) return; + if (loginStatus === "notLoggedIn") return; + + const previousLoginStatus = previousLoginStatusRef.current; + previousLoginStatusRef.current = loginStatus; + + if (roomStatus === "idle") { + roomDispatch({ type: "PrivateRoomInit" }); + return; + } + + if (previousLoginStatus !== loginStatus && roomStatus !== "loading") { + roomDispatch({ type: "PrivateRoomRefresh" }); + } + }, [ + hasInitialized, + isAuthLoading, + loginStatus, + roomDispatch, + roomStatus, + ]); +} + +export function usePrivateRoomUnlockPaywallNavigation({ + loginStatus, + roomDispatch, + unlockPaywallRequest, +}: UsePrivateRoomUnlockPaywallNavigationInput): void { + const navigator = useAppNavigator(); + + useEffect(() => { + if (!unlockPaywallRequest) return; + + if (isPrivateRoomAuthRequired(loginStatus)) { + navigator.openAuth(ROUTES.privateRoom); + } else { + navigator.openSubscription({ type: "topup" }); + } + roomDispatch({ type: "PrivateRoomUnlockPaywallConsumed" }); + }, [loginStatus, navigator, roomDispatch, unlockPaywallRequest]); +} + +export function usePrivateRoomUnlockSuccessRefresh( + unlockSuccessNonce: number, +): void { + const userDispatch = useUserDispatch(); + const lastUnlockSuccessNonceRef = useRef(0); + + useEffect(() => { + if (unlockSuccessNonce <= lastUnlockSuccessNonceRef.current) return; + lastUnlockSuccessNonceRef.current = unlockSuccessNonce; + userDispatch({ type: "UserFetch" }); + }, [unlockSuccessNonce, userDispatch]); +} diff --git a/src/app/sidebar/page.tsx b/src/app/sidebar/page.tsx index 7ad4f0f6..bb967dc8 100644 --- a/src/app/sidebar/page.tsx +++ b/src/app/sidebar/page.tsx @@ -1,5 +1,3 @@ -"use client"; - import { SidebarScreen } from "@/app/sidebar/sidebar-screen"; export default function SidebarPage() { diff --git a/src/app/subscription/page.tsx b/src/app/subscription/page.tsx index 6ccd066f..452dfc6c 100644 --- a/src/app/subscription/page.tsx +++ b/src/app/subscription/page.tsx @@ -1,6 +1,6 @@ import { Suspense } from "react"; -import { MobileShell } from "@/app/_components/core"; +import { PageLoadingFallback } from "@/app/_components/core"; import { SubscriptionPageClient } from "./subscription-page-client"; @@ -14,19 +14,6 @@ export default function SubscriptionPage() { function SubscriptionFallback() { return ( - - - Loading subscription... - - + Loading subscription... ); } diff --git a/src/app/subscription/use-subscription-payment-flow.ts b/src/app/subscription/use-subscription-payment-flow.ts index 3bc2d60a..58a5bbfa 100644 --- a/src/app/subscription/use-subscription-payment-flow.ts +++ b/src/app/subscription/use-subscription-payment-flow.ts @@ -2,16 +2,12 @@ import { useEffect, useRef, useState } from "react"; -import { - clearPendingPaymentOrder, - getPendingPaymentOrderForType, -} from "@/lib/payment/pending_payment_order"; +import { usePaymentOrderLifecycle } from "@/app/_hooks/use-payment-order-lifecycle"; import { useAppNavigator } from "@/router/use-app-navigator"; import { usePaymentDispatch, usePaymentState, } from "@/stores/payment/payment-context"; -import { useUserDispatch } from "@/stores/user/user-context"; import type { PayChannel } from "@/data/dto/payment"; import type { SubscriptionType } from "./subscription-screen.helpers"; @@ -30,16 +26,20 @@ export function useSubscriptionPaymentFlow({ initialPayChannel, }: UseSubscriptionPaymentFlowInput) { const navigator = useAppNavigator(); - const userDispatch = useUserDispatch(); const payment = usePaymentState(); const paymentDispatch = usePaymentDispatch(); - const refreshedPaidOrderRef = useRef(null); - const resumedPendingOrderRef = useRef(null); const successDialogShownOrderRef = useRef(null); const initialPayChannelAppliedRef = useRef(false); const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] = useState(false); + usePaymentOrderLifecycle({ + payment, + paymentDispatch, + paymentType: subscriptionType, + shouldResumePendingOrder, + }); + useEffect(() => { if (payment.status === "idle") { initialPayChannelAppliedRef.current = true; @@ -68,13 +68,6 @@ export function useSubscriptionPaymentFlow({ paymentDispatch, ]); - useEffect(() => { - if (!payment.isPaid || !payment.currentOrderId) return; - if (refreshedPaidOrderRef.current === payment.currentOrderId) return; - refreshedPaidOrderRef.current = payment.currentOrderId; - userDispatch({ type: "UserFetch" }); - }, [payment.currentOrderId, payment.isPaid, userDispatch]); - useEffect(() => { if (!payment.isPaid || !payment.currentOrderId) return; if (successDialogShownOrderRef.current === payment.currentOrderId) return; @@ -83,66 +76,6 @@ export function useSubscriptionPaymentFlow({ setShowPaymentSuccessDialog(true); }, [payment.currentOrderId, payment.isPaid]); - useEffect(() => { - const canInspectPendingOrder = - payment.status === "ready" || - (!shouldResumePendingOrder && - (payment.isPollingOrder || - payment.isPaid || - payment.status === "failed")); - if (!canInspectPendingOrder) return; - - let cancelled = false; - - const handlePendingOrder = async () => { - const result = await getPendingPaymentOrderForType(subscriptionType); - if (cancelled || !result.success || result.data === null) return; - - if (!shouldResumePendingOrder) { - await clearPendingPaymentOrder(); - if ( - payment.currentOrderId === result.data.orderId && - (payment.isPollingOrder || - payment.isPaid || - payment.status === "failed") - ) { - paymentDispatch({ type: "PaymentReset" }); - } - return; - } - - if (payment.currentOrderId === result.data.orderId) return; - if (resumedPendingOrderRef.current === result.data.orderId) return; - - resumedPendingOrderRef.current = result.data.orderId; - paymentDispatch({ - type: "PaymentReturned", - orderId: result.data.orderId, - createdAt: result.data.createdAt, - }); - }; - - void handlePendingOrder(); - return () => { - cancelled = true; - }; - }, [ - payment.currentOrderId, - payment.isPaid, - payment.isPollingOrder, - payment.status, - paymentDispatch, - shouldResumePendingOrder, - subscriptionType, - ]); - - useEffect(() => { - if (!payment.currentOrderId) return; - if (!payment.isPaid && payment.status !== "failed") return; - - void clearPendingPaymentOrder(); - }, [payment.currentOrderId, payment.isPaid, payment.status]); - const handleBackClick = () => { navigator.exitSubscription(returnTo); }; diff --git a/src/app/tip/page.tsx b/src/app/tip/page.tsx index 13e86e18..312fba44 100644 --- a/src/app/tip/page.tsx +++ b/src/app/tip/page.tsx @@ -1,6 +1,6 @@ import { Suspense } from "react"; -import { MobileShell } from "@/app/_components/core"; +import { PageLoadingFallback } from "@/app/_components/core"; import { TipPageClient } from "./tip-page-client"; @@ -14,18 +14,8 @@ export default function TipPage() { function TipFallback() { return ( - - - Loading tip... - - + + Loading tip... + ); } diff --git a/src/app/tip/tip-screen.tsx b/src/app/tip/tip-screen.tsx index 0949ee4b..19bf35a1 100644 --- a/src/app/tip/tip-screen.tsx +++ b/src/app/tip/tip-screen.tsx @@ -5,11 +5,8 @@ import Image from "next/image"; import { ArrowLeft, Heart, Sparkles } from "lucide-react"; import { MobileShell } from "@/app/_components/core"; +import { usePaymentOrderLifecycle } from "@/app/_hooks/use-payment-order-lifecycle"; import type { PayChannel } from "@/data/dto/payment"; -import { - clearPendingPaymentOrder, - getPendingPaymentOrderForType, -} from "@/lib/payment/pending_payment_order"; import { ROUTES } from "@/router/routes"; import { useAppNavigator } from "@/router/use-app-navigator"; import { useAuthState } from "@/stores/auth/auth-context"; @@ -17,7 +14,7 @@ import { usePaymentDispatch, usePaymentState, } from "@/stores/payment/payment-context"; -import { useUserDispatch, useUserState } from "@/stores/user/user-context"; +import { useUserState } from "@/stores/user/user-context"; import { TipCheckoutButton } from "./tip-checkout-button"; import { @@ -39,12 +36,9 @@ export function TipScreen({ const navigator = useAppNavigator(); const authState = useAuthState(); const userState = useUserState(); - const userDispatch = useUserDispatch(); const payment = usePaymentState(); const paymentDispatch = usePaymentDispatch(); const initialPayChannelAppliedRef = useRef(false); - const resumedPendingOrderRef = useRef(null); - const refreshedPaidOrderRef = useRef(null); const resolvedInitialPayChannel = initialPayChannel ?? navigator.getDefaultPayChannel(); @@ -66,6 +60,13 @@ export function TipScreen({ payment.status === "ready" && !payment.isLoadingPlans && coffeePlan === null; const isAuthLoading = !authState.hasInitialized || authState.isLoading; + usePaymentOrderLifecycle({ + payment, + paymentDispatch, + paymentType: "tip", + shouldResumePendingOrder, + }); + useEffect(() => { if (payment.status === "idle") { initialPayChannelAppliedRef.current = true; @@ -124,72 +125,6 @@ export function TipScreen({ paymentDispatch, ]); - useEffect(() => { - const canInspectPendingOrder = - payment.status === "ready" || - (!shouldResumePendingOrder && - (payment.isPollingOrder || - payment.isPaid || - payment.status === "failed")); - if (!canInspectPendingOrder) return; - - let cancelled = false; - - const handlePendingOrder = async () => { - const result = await getPendingPaymentOrderForType("tip"); - if (cancelled || !result.success || result.data === null) return; - - if (!shouldResumePendingOrder) { - await clearPendingPaymentOrder(); - if ( - payment.currentOrderId === result.data.orderId && - (payment.isPollingOrder || - payment.isPaid || - payment.status === "failed") - ) { - paymentDispatch({ type: "PaymentReset" }); - } - return; - } - - if (payment.currentOrderId === result.data.orderId) return; - if (resumedPendingOrderRef.current === result.data.orderId) return; - - resumedPendingOrderRef.current = result.data.orderId; - paymentDispatch({ - type: "PaymentReturned", - orderId: result.data.orderId, - createdAt: result.data.createdAt, - }); - }; - - void handlePendingOrder(); - return () => { - cancelled = true; - }; - }, [ - payment.currentOrderId, - payment.isPaid, - payment.isPollingOrder, - payment.status, - paymentDispatch, - shouldResumePendingOrder, - ]); - - useEffect(() => { - if (!payment.currentOrderId) return; - if (!payment.isPaid && payment.status !== "failed") return; - - void clearPendingPaymentOrder(); - }, [payment.currentOrderId, payment.isPaid, payment.status]); - - useEffect(() => { - if (!payment.isPaid || !payment.currentOrderId) return; - if (refreshedPaidOrderRef.current === payment.currentOrderId) return; - refreshedPaidOrderRef.current = payment.currentOrderId; - userDispatch({ type: "UserFetch" }); - }, [payment.currentOrderId, payment.isPaid, userDispatch]); - const handleOrder = () => { if (isAuthLoading) return; if (!isRealLoginStatus(authState.loginStatus)) {