Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions frontend/src/app/pay/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { motion } from "framer-motion";
import Image from "next/image";
import { useParams, useRouter } from "next/navigation";
import { useLocale, useTranslations } from "next-intl";
Expand Down Expand Up @@ -262,7 +263,10 @@ export default function PaymentPage() {
useEffect(() => {
if (!walletPublicKey) return;
if (didWalletAccountSwitch(previousWalletPublicKey.current, walletPublicKey)) {
toast.info("Wallet account switched. Checkout balances updated.");
// Optimistic update: clear balances immediately when wallet switches
setWalletBalances([]);
setSortedSourceAssets([]);
toast.info("Wallet account switched. Updating balances...");
}
previousWalletPublicKey.current = walletPublicKey;
}, [walletPublicKey]);
Expand Down Expand Up @@ -501,7 +505,13 @@ export default function PaymentPage() {
{sortedSourceAssets.length > 0 && (
<div className="flex flex-col gap-1.5">
<label className="text-[10px] font-bold uppercase tracking-widest text-[#6B6B6B]">Payment Asset</label>
<div className="relative">
<motion.div
className="relative"
key={walletBalances.length} // Re-animate when balances update
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
>
<select value={sourceAsset} onChange={(e) => setSourceAsset(e.target.value)}
className="w-full appearance-none rounded-xl border border-[#E8E8E8] bg-[#F9F9F9] px-4 py-3 text-sm font-medium text-[#0A0A0A] focus:border-[#0A0A0A] focus:outline-none transition-colors">
{sortedSourceAssets.map(code => (
Expand All @@ -511,7 +521,7 @@ export default function PaymentPage() {
<div className="pointer-events-none absolute inset-y-0 right-3 flex items-center text-[#6B6B6B]">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</div>
</div>
</motion.div>
</div>
)}

Expand Down
65 changes: 47 additions & 18 deletions frontend/src/components/OnboardingProgressTracker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { useState, useCallback, useMemo, useEffect } from "react";
import React, { useState, useCallback, useMemo, useEffect, useReducer } from "react";
import { motion, AnimatePresence, type Variants } from "framer-motion";
import { useTranslations } from "next-intl";

Expand Down Expand Up @@ -29,6 +29,35 @@ interface OnboardingProgressTrackerProps {
compact?: boolean;
}

/**
* State for onboarding tracker
*/
interface OnboardingState {
currentStep: string | undefined;
announcementText: string;
}

/**
* Actions for onboarding state
*/
type OnboardingAction =
| { type: "SET_CURRENT_STEP"; payload: string }
| { type: "SET_ANNOUNCEMENT"; payload: string };

/**
* Reducer for onboarding state
*/
function onboardingReducer(state: OnboardingState, action: OnboardingAction): OnboardingState {
switch (action.type) {
case "SET_CURRENT_STEP":
return { ...state, currentStep: action.payload };
case "SET_ANNOUNCEMENT":
return { ...state, announcementText: action.payload };
default:
return state;
}
}

/**
* Animation variants for step container
*/
Expand Down Expand Up @@ -107,10 +136,10 @@ export const OnboardingProgressTracker: React.FC<
compact = false,
}) => {
const t = useTranslations();
const [currentStep, setCurrentStep] = useState<string | undefined>(
currentStepProp || steps[0]?.id
);
const [announcementText, setAnnouncementText] = useState<string>("");
const [state, dispatch] = useReducer(onboardingReducer, {
currentStep: currentStepProp || steps[0]?.id,
announcementText: "",
});

/**
* Sort steps by order
Expand Down Expand Up @@ -145,11 +174,11 @@ export const OnboardingProgressTracker: React.FC<
if (!step) return;

// Optimistic update: immediately update current step
setCurrentStep(stepId);
dispatch({ type: "SET_CURRENT_STEP", payload: stepId });

// Announce to screen readers immediately
const announcement = `${t("onboarding.stepProgress") || "Step"} ${step.order}: ${step.title}. ${step.description}`;
setAnnouncementText(announcement);
dispatch({ type: "SET_ANNOUNCEMENT", payload: announcement });

// Call the callback (which may handle server-side updates)
onStepChange?.(stepId);
Expand All @@ -163,7 +192,7 @@ export const OnboardingProgressTracker: React.FC<
useEffect(() => {
if (isOnboardingComplete && sortedSteps.length > 0) {
const announcement = t("onboarding.completed") || "Onboarding completed";
setAnnouncementText(announcement);
dispatch({ type: "SET_ANNOUNCEMENT", payload: announcement });
onComplete?.();
}
}, [isOnboardingComplete, sortedSteps.length, onComplete, t]);
Expand All @@ -173,7 +202,7 @@ export const OnboardingProgressTracker: React.FC<
*/
useEffect(() => {
const StatusMessage = `${t("onboarding.progress") || "Progress"}: ${progressPercentage}% complete`;
setAnnouncementText(StatusMessage);
dispatch({ type: "SET_ANNOUNCEMENT", payload: StatusMessage });
}, [progressPercentage, t]);

return (
Expand All @@ -185,14 +214,14 @@ export const OnboardingProgressTracker: React.FC<
aria-atomic="false"
>
{/* Screen reader announcement area */}
<div
className="sr-only"
role="status"
aria-live="assertive"
aria-atomic="true"
>
{announcementText}
</div>
<div
className="sr-only"
role="status"
aria-live="assertive"
aria-atomic="true"
>
{state.announcementText}
</div>

{/* Container */}
<div
Expand Down Expand Up @@ -268,7 +297,7 @@ export const OnboardingProgressTracker: React.FC<
>
<AnimatePresence mode="popLayout">
{sortedSteps.map((step, index) => {
const isCurrentStep = currentStep === step.id;
const isCurrentStep = state.currentStep === step.id;

return (
<motion.div
Expand Down
Loading
Loading