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
10 changes: 5 additions & 5 deletions fix.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[Frontend] Improve screen reader support for Multi-sig Approval Modal
[Frontend] Enable optimistic updates in Multi-sig Approval Modal
Repo Avatar
emdevelopa/Stellar_Payment_API
Description
This task involves UX enhancement for the Multi-sig Approval Modal module.
The goal is to improve screen reader support for Multi-sig Approval Modal to improve the platform's styling, user interactions, and overall accessibility.
The goal is to enable optimistic updates in Multi-sig Approval Modal to improve the platform's styling, user interactions, and overall accessibility.

Requirements and context

Expand All @@ -14,15 +14,15 @@ Specifically focused on frontend UX enhancement and responsiveness
Suggested execution

Fork the repo and create a branch
git checkout -b feature/fe-improve-screen-reader-support-for-multi-sig-approval-modal
git checkout -b feature/fe-enable-optimistic-updates-in-multi-sig-approval-modal
Implement changes

Review existing component in Multi-sig Approval Modal
Apply changes: Improve screen reader support for Multi-sig Approval Modal
Apply changes: Enable optimistic updates in Multi-sig Approval Modal
Use clean CSS or tailwind variables for styling
Maintain state transitions and visual feedback
Test and commit

Test mobile responsiveness and interactive states
Check accessibility (a11y) using standard audits
Include screenshots or gifs in the PR
Include screenshots or gifs in the PR
24 changes: 24 additions & 0 deletions frontend/src/components/MultisigApprovalModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,30 @@ describe("MultisigApprovalModal Component", () => {
});

describe("Confirmation State", () => {
it("shows optimistic pending state on submit before finalising", async () => {
renderWithProvider();

const signButtons = screen.getAllByText("Sign");
fireEvent.click(signButtons[0]);

await waitFor(() => {
expect(screen.getByText("50%")).toBeInTheDocument();
}, { timeout: 2000 });

fireEvent.click(signButtons[1]);

await waitFor(() => {
expect(screen.getByText("Submit Transaction")).toBeInTheDocument();
}, { timeout: 2000 });

fireEvent.click(screen.getByText("Submit Transaction"));

// Optimistic state appears immediately
expect(screen.getByText("Transaction Submitted")).toBeInTheDocument();
expect(screen.getByText("Awaiting network confirmation...")).toBeInTheDocument();
expect(screen.getByText("Confirming...")).toBeInTheDocument();
});

it("shows confirmation after successful submission", async () => {
renderWithProvider();

Expand Down
97 changes: 67 additions & 30 deletions frontend/src/components/MultisigApprovalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default function MultisigApprovalModal({
requiredSignatures,
progress,
isExpired,
isPendingConfirmation,
timeRemaining,
} = useMultisigState();

Expand Down Expand Up @@ -155,10 +156,10 @@ export default function MultisigApprovalModal({
}, [isOpen]);

const handleClose = useCallback(() => {
if (isLoading) return;
if (isLoading || isPendingConfirmation) return;
resetModal();
onClose();
}, [isLoading, resetModal, onClose]);
}, [isLoading, isPendingConfirmation, resetModal, onClose]);

const handleSign = useCallback(async (signerId: string) => {
try {
Expand Down Expand Up @@ -347,36 +348,72 @@ export default function MultisigApprovalModal({

const ConfirmStep = () => (
<div className="text-center space-y-6">
<div className="w-16 h-16 bg-mint/20 rounded-full flex items-center justify-center mx-auto">
<svg className="w-8 h-8 text-mint" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<h3 className="text-xl font-bold text-white">Transaction Approved</h3>
<p className="mt-2 text-sm text-slate-400">
Your multi-signature transaction has been successfully submitted
</p>
</div>
{transaction?.submittedTxHash && (
<div className="rounded-xl border border-mint/30 bg-mint/5 p-4">
<p className="text-xs font-medium uppercase tracking-wider text-mint mb-2">Transaction Hash</p>
<div className="flex items-center justify-center gap-2">
<code className="font-mono text-sm text-slate-200">
{transaction.submittedTxHash}
</code>
<CopyButton text={transaction.submittedTxHash} />
{isPendingConfirmation ? (
<>
<div className="relative mx-auto w-16 h-16" aria-hidden="true">
<div className="w-16 h-16 border-4 border-mint border-t-transparent rounded-full animate-spin" />
<div className="absolute inset-0 w-16 h-16 border-4 border-mint/20 rounded-full animate-ping" />
</div>
</div>
<div>
<h3 className="text-xl font-bold text-white">Transaction Submitted</h3>
<p className="mt-2 text-sm text-slate-400">
Awaiting network confirmation...
</p>
</div>
{transaction?.submittedTxHash && (
<div className="rounded-xl border border-mint/30 bg-mint/5 p-4">
<p className="text-xs font-medium uppercase tracking-wider text-mint mb-2">
Transaction Hash (pending)
</p>
<div className="flex items-center justify-center gap-2">
<code className="font-mono text-sm text-slate-200">
{transaction.submittedTxHash}
</code>
<CopyButton text={transaction.submittedTxHash} />
</div>
</div>
)}
<motion.button
disabled
className="px-6 py-2 bg-mint/50 text-black/50 font-semibold rounded-xl cursor-not-allowed"
>
Confirming...
</motion.button>
</>
) : (
<>
<div className="w-16 h-16 bg-mint/20 rounded-full flex items-center justify-center mx-auto">
<svg className="w-8 h-8 text-mint" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div>
<h3 className="text-xl font-bold text-white">Transaction Approved</h3>
<p className="mt-2 text-sm text-slate-400">
Your multi-signature transaction has been successfully submitted
</p>
</div>
{transaction?.submittedTxHash && (
<div className="rounded-xl border border-mint/30 bg-mint/5 p-4">
<p className="text-xs font-medium uppercase tracking-wider text-mint mb-2">Transaction Hash</p>
<div className="flex items-center justify-center gap-2">
<code className="font-mono text-sm text-slate-200">
{transaction.submittedTxHash}
</code>
<CopyButton text={transaction.submittedTxHash} />
</div>
</div>
)}
<motion.button
onClick={handleClose}
whileHover={!prefersReducedMotion ? { scale: 1.02 } : undefined}
whileTap={!prefersReducedMotion ? { scale: 0.98 } : undefined}
className="px-6 py-2 bg-mint text-black font-semibold rounded-xl hover:bg-glow transition-colors"
>
Close
</motion.button>
</>
)}
<motion.button
onClick={handleClose}
whileHover={!prefersReducedMotion ? { scale: 1.02 } : undefined}
whileTap={!prefersReducedMotion ? { scale: 0.98 } : undefined}
className="px-6 py-2 bg-mint text-black font-semibold rounded-xl hover:bg-glow transition-colors"
>
Close
</motion.button>
</div>
);

Expand Down
153 changes: 98 additions & 55 deletions frontend/src/lib/multisig-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface MultisigContextType {
transaction: MultisigTransaction | null;
currentStep: MultisigStep;
isLoading: boolean;
isPendingConfirmation: boolean;
error: string | null;
isMounted: boolean;
isVisible: boolean;
Expand Down Expand Up @@ -173,19 +174,26 @@ interface MultisigProviderProps {
readonly networkPassphrase: string;
}

export function MultisigProvider({ children, networkPassphrase: _networkPassphrase }: MultisigProviderProps) {
const [state, dispatch] = useReducer(multisigReducer, INITIAL_STATE);

// Always-current snapshot for use inside async callbacks without stale-closure issues.
const stateRef = useRef(state);
stateRef.current = state;
export function MultisigProvider({ children, networkPassphrase }: MultisigProviderProps) {
const [transaction, setTransaction] = useState<MultisigTransaction | null>(null);
const [currentStep, setCurrentStep] = useState<MultisigStep>("review");
const [isLoading, setIsLoading] = useState(false);
const [isPendingConfirmation, setIsPendingConfirmation] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isMounted, setIsMounted] = useState(false);
const [isVisible, setIsVisible] = useState(false);

const clearError = useCallback(() => {
dispatch({ type: "CLEAR_ERROR" });
}, []);

const resetModal = useCallback(() => {
dispatch({ type: "RESET" });
setTransaction(null);
setCurrentStep("review");
setIsLoading(false);
setIsPendingConfirmation(false);
setError(null);
setIsVisible(false);
}, []);

const setTransactionSafe = useCallback((newTransaction: MultisigTransaction | null) => {
Expand Down Expand Up @@ -256,27 +264,54 @@ export function MultisigProvider({ children, networkPassphrase: _networkPassphra
return;
}

const signedWeight = transaction.signers
.filter((s) => s.hasSigned)
.reduce((sum, s) => sum + s.weight, 0);
if (signedWeight < transaction.minSignatures) {
dispatch({ type: "SET_ERROR", payload: "Not enough signatures to submit transaction" });
return;
}
const previousTransaction = { ...transaction, signers: [...transaction.signers] };

dispatch({ type: "SET_LOADING", payload: true });
dispatch({ type: "CLEAR_ERROR" });
dispatch({ type: "SET_STEP", payload: "processing" });
try {
clearError();

// Verify enough signatures
const signedWeight = transaction.signers.filter(s => s.hasSigned)
.reduce((sum, s) => sum + s.weight, 0);

if (signedWeight < transaction.minSignatures) {
throw new Error("Not enough signatures to submit transaction");
}

// Optimistic update: immediately show confirm step with a pending tx hash
const pendingTxHash = `tx_pending_${Date.now()}`;
setIsPendingConfirmation(true);
setCurrentStep("confirm");
setTransactionSafe({
...transaction,
status: 'approved' as MultisigApprovalStatus,
submittedTxHash: pendingTxHash,
});

// Simulate submission process
await new Promise(resolve => setTimeout(resolve, 2000));

// Finalize with real transaction hash
const realTxHash = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
setTransactionSafe({
...transaction,
status: 'approved' as MultisigApprovalStatus,
submittedTxHash: realTxHash,
});

try {
await new Promise((resolve) => setTimeout(resolve, 2000));
const txHash = `tx_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
dispatch({ type: "SUBMIT_SUCCESS", txHash });
} catch (err) {
// Revert optimistic update
setTransactionSafe(previousTransaction);
const errorMessage = err instanceof Error ? err.message : "Failed to submit transaction";
dispatch({ type: "SET_ERROR", payload: errorMessage });
dispatch({ type: "SET_STEP", payload: "error" });
console.error("Submission error:", err);
} finally {
setIsLoading(false);
setIsPendingConfirmation(false);
}
}, []);

Expand Down Expand Up @@ -332,38 +367,44 @@ export function MultisigProvider({ children, networkPassphrase: _networkPassphra
};
}, [transaction, currentStep]);

const value: MultisigContextType = useMemo(
() => ({
transaction,
currentStep,
isLoading,
error,
isMounted,
isVisible,
setTransaction: setTransactionSafe,
setCurrentStep: (step: MultisigStep) => dispatch({ type: "SET_STEP", payload: step }),
signTransaction,
submitTransaction,
resetModal,
clearError,
retryAction,
...computedValues,
}),
[
transaction,
currentStep,
isLoading,
error,
isMounted,
isVisible,
setTransactionSafe,
signTransaction,
submitTransaction,
resetModal,
clearError,
retryAction,
computedValues,
],
const value: MultisigContextType = useMemo(() => ({
transaction,
currentStep,
isLoading,
isPendingConfirmation,
error,
isMounted,
isVisible,
setTransaction: setTransactionSafe,
setCurrentStep,
signTransaction,
submitTransaction,
resetModal,
clearError,
retryAction,
...computedValues
}), [
transaction,
currentStep,
isLoading,
isPendingConfirmation,
error,
isMounted,
isVisible,
setTransactionSafe,
setCurrentStep,
signTransaction,
submitTransaction,
resetModal,
clearError,
retryAction,
computedValues
]);

return (
<MultisigContext.Provider value={value}>
{children}
</MultisigContext.Provider>
);

return <MultisigContext.Provider value={value}>{children}</MultisigContext.Provider>;
Expand All @@ -380,12 +421,13 @@ export function useMultisig() {
}

export function useMultisigState() {
const {
transaction,
currentStep,
isLoading,
error,
isMounted,
const {
transaction,
currentStep,
isLoading,
isPendingConfirmation,
error,
isMounted,
isVisible,
canSign,
canSubmit,
Expand All @@ -400,6 +442,7 @@ export function useMultisigState() {
transaction,
currentStep,
isLoading,
isPendingConfirmation,
error,
isMounted,
isVisible,
Expand Down
Loading
Loading