Skip to content

Commit 08fe449

Browse files
authored
Merge pull request #1029 from Frontman-Code/feature/fe-refactor-state-logic-for-payment-success-animation
feat(frontend): refactor state logic for Payment Success Animation
2 parents 2126d20 + 035f7e9 commit 08fe449

4 files changed

Lines changed: 63 additions & 17 deletions

File tree

frontend/package-lock.json

-808 KB
Binary file not shown.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"devDependencies": {
5757
"@axe-core/cli": "^4.11.1",
5858
"@playwright/test": "^1.58.2",
59+
"@testing-library/dom": "^10.4.1",
5960
"@testing-library/jest-dom": "^6.9.1",
6061
"@testing-library/react": "^16.3.2",
6162
"@testing-library/user-event": "^14.6.1",

frontend/src/app/pay/[id]/page.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,6 @@ export default function PaymentPage() {
185185
useEffect(() => {
186186
if (payment && (payment.status === "confirmed" || payment.status === "completed")) {
187187
setIsOptimisticSuccess(true);
188-
// Auto-hide the big celebration after 4 seconds to show the receipt/details
189-
const timer = setTimeout(() => setIsOptimisticSuccess(false), 4000);
190-
return () => clearTimeout(timer);
191188
}
192189
}, [payment]);
193190

frontend/src/components/PaymentSuccessAnimation.tsx

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import React, { useCallback, useEffect, useRef, useState } from "react";
3+
import React, { useCallback, useEffect, useRef, useReducer } from "react";
44
import { motion, AnimatePresence, useReducedMotion, type Variants } from "framer-motion";
55
import confetti from "canvas-confetti";
66
import { useTranslations } from "next-intl";
@@ -71,6 +71,55 @@ const checkVariantsReduced: Variants = {
7171
visible: { opacity: 1, transition: { duration: 0.2 } },
7272
};
7373

74+
interface AnimationState {
75+
isDismissing: boolean;
76+
announcementText: string;
77+
}
78+
79+
type AnimationAction =
80+
| { type: "RESET" }
81+
| { type: "SET_ANNOUNCEMENT"; payload: string }
82+
| { type: "START_DISMISS"; payload: string }
83+
| { type: "DISMISS_ERROR"; payload: string }
84+
| { type: "DISMISS_SUCCESS" };
85+
86+
const initialState: AnimationState = {
87+
isDismissing: false,
88+
announcementText: "",
89+
};
90+
91+
function animationReducer(state: AnimationState, action: AnimationAction): AnimationState {
92+
switch (action.type) {
93+
case "RESET":
94+
return {
95+
isDismissing: false,
96+
announcementText: "",
97+
};
98+
case "SET_ANNOUNCEMENT":
99+
return {
100+
...state,
101+
announcementText: action.payload,
102+
};
103+
case "START_DISMISS":
104+
return {
105+
isDismissing: true,
106+
announcementText: action.payload,
107+
};
108+
case "DISMISS_ERROR":
109+
return {
110+
isDismissing: false,
111+
announcementText: action.payload,
112+
};
113+
case "DISMISS_SUCCESS":
114+
return {
115+
...state,
116+
isDismissing: false,
117+
};
118+
default:
119+
return state;
120+
}
121+
}
122+
74123
export const PaymentSuccessAnimation: React.FC<PaymentSuccessAnimationProps> = ({
75124
show,
76125
onComplete,
@@ -88,44 +137,43 @@ export const PaymentSuccessAnimation: React.FC<PaymentSuccessAnimationProps> = (
88137
const containerRef = useRef<HTMLDivElement>(null);
89138
const prevIsOptimisticRef = useRef(isOptimistic);
90139

91-
// Optimistic dismiss state (#981)
92-
const [isDismissing, setIsDismissing] = useState(false);
93-
// Dynamic live-region text for state transitions (#980, #981)
94-
const [announcementText, setAnnouncementText] = useState("");
140+
const [state, dispatch] = useReducer(animationReducer, initialState);
141+
const { isDismissing, announcementText } = state;
95142

96143
// Reset dismiss state when dialog closes
97144
useEffect(() => {
98145
if (!show) {
99-
setIsDismissing(false);
100-
setAnnouncementText("");
146+
dispatch({ type: "RESET" });
101147
}
102148
}, [show]);
103149

104150
// Announce network confirmation when optimistic state resolves (#980)
105151
useEffect(() => {
106152
if (!show) return;
107153
if (prevIsOptimisticRef.current === true && !isOptimistic) {
108-
setAnnouncementText(
109-
t("payment.networkConfirmed") || "Payment confirmed on the Stellar network."
110-
);
154+
dispatch({
155+
type: "SET_ANNOUNCEMENT",
156+
payload: t("payment.networkConfirmed") || "Payment confirmed on the Stellar network.",
157+
});
111158
}
112159
prevIsOptimisticRef.current = isOptimistic;
113160
}, [isOptimistic, show, t]);
114161

115162
// Optimistic dismiss handler — UI responds immediately, rolls back on error (#981)
116163
const handleDismiss = useCallback(async () => {
117164
if (isDismissing) return;
118-
setIsDismissing(true);
119-
setAnnouncementText(t("payment.dismissing") || "Processing…");
165+
dispatch({
166+
type: "START_DISMISS",
167+
payload: t("payment.dismissing") || "Processing…",
168+
});
120169
try {
121170
await onComplete?.();
122171
} catch (err) {
123172
const msg =
124173
err instanceof Error
125174
? err.message
126175
: t("common.error") || "Something went wrong. Please try again.";
127-
setIsDismissing(false);
128-
setAnnouncementText(msg);
176+
dispatch({ type: "DISMISS_ERROR", payload: msg });
129177
}
130178
}, [isDismissing, onComplete, t]);
131179

0 commit comments

Comments
 (0)