From 3eeee1f4ac6c493adac51e15b161b02cc05e7f66 Mon Sep 17 00:00:00 2001 From: emmanard Date: Mon, 1 Jun 2026 10:04:59 +0100 Subject: [PATCH 1/2] fix: unblock pre-push tests after upstream merge Fix withdrawal lock release and payment test token cache resets. --- jest.config.js | 1 + lib/kyc/withdrawalLimitService.ts | 13 +++++++++---- lib/payments/__tests__/mpesa.test.ts | 3 ++- lib/payments/__tests__/mtn-momo.test.ts | 3 ++- lib/payments/mpesa.ts | 11 ++++++++--- lib/payments/mtn-momo.ts | 5 +++++ 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/jest.config.js b/jest.config.js index d723614..5ce576d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -30,6 +30,7 @@ const customJestConfig = { }, }, testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'], + testPathIgnorePatterns: ['/node_modules/', '/.next/', '/helpcenter/'], } module.exports = createJestConfig(customJestConfig) diff --git a/lib/kyc/withdrawalLimitService.ts b/lib/kyc/withdrawalLimitService.ts index 26b8ea4..ab371e5 100644 --- a/lib/kyc/withdrawalLimitService.ts +++ b/lib/kyc/withdrawalLimitService.ts @@ -66,20 +66,25 @@ export const _withdrawalStore = new Map() const _userLocks = new Map>() function _acquireLock(userId: string): { release: () => void; ready: Promise } { - let release!: () => void + let releaseFn: (() => void) | null = null const ready = new Promise((resolve) => { const prev = _userLocks.get(userId) ?? Promise.resolve() _userLocks.set( userId, prev.then(() => { resolve() - return new Promise((r) => { - release = r + return new Promise((resolveRelease) => { + releaseFn = resolveRelease }) }) ) }) - return { ready, release } + return { + ready, + release: () => { + releaseFn?.() + }, + } } // --------------------------------------------------------------------------- diff --git a/lib/payments/__tests__/mpesa.test.ts b/lib/payments/__tests__/mpesa.test.ts index 89d24bd..ef35ec4 100644 --- a/lib/payments/__tests__/mpesa.test.ts +++ b/lib/payments/__tests__/mpesa.test.ts @@ -2,7 +2,7 @@ * Tests for the M-Pesa STK Push integration. */ -import { MpesaProvider } from '../mpesa' +import { MpesaProvider, resetMpesaTokenCacheForTests } from '../mpesa' import { MobileMoneyError } from '../types' // --------------------------------------------------------------------------- @@ -38,6 +38,7 @@ beforeEach(() => { process.env.MPESA_SHORTCODE = '174379' process.env.MPESA_PASSKEY = 'test-passkey' process.env.MPESA_ENV = 'sandbox' + resetMpesaTokenCacheForTests() jest.useFakeTimers() }) diff --git a/lib/payments/__tests__/mtn-momo.test.ts b/lib/payments/__tests__/mtn-momo.test.ts index 9789056..2da4ccb 100644 --- a/lib/payments/__tests__/mtn-momo.test.ts +++ b/lib/payments/__tests__/mtn-momo.test.ts @@ -2,7 +2,7 @@ * Tests for the MTN MoMo Collections API integration. */ -import { MtnMomoProvider } from '../mtn-momo' +import { MtnMomoProvider, resetMtnMomoTokenCacheForTests } from '../mtn-momo' import { MobileMoneyError } from '../types' // --------------------------------------------------------------------------- @@ -37,6 +37,7 @@ beforeEach(() => { process.env.MTN_MOMO_API_USER = '550e8400-e29b-41d4-a716-446655440000' process.env.MTN_MOMO_API_KEY = 'test-api-key' process.env.MTN_MOMO_ENV = 'sandbox' + resetMtnMomoTokenCacheForTests() }) afterEach(() => { diff --git a/lib/payments/mpesa.ts b/lib/payments/mpesa.ts index 64f546b..b481bab 100644 --- a/lib/payments/mpesa.ts +++ b/lib/payments/mpesa.ts @@ -41,6 +41,11 @@ interface TokenCache { let tokenCache: TokenCache | null = null +/** Clears cached OAuth token (for tests). */ +export function resetMpesaTokenCacheForTests(): void { + tokenCache = null +} + async function fetchAccessToken(): Promise { const now = Date.now() if (tokenCache && tokenCache.expiresAt > now + 30_000) { @@ -226,7 +231,7 @@ async function pollForResult(checkoutRequestId: string): Promise // ResponseCode 0 means the query itself succeeded; ResultCode is the payment outcome if (result.ResponseCode === '0') { - const status = mapResultCode(result.ResultCode) + const status = mapResultCode(String(result.ResultCode)) if (status !== 'PENDING') { return status } @@ -273,11 +278,11 @@ export class MpesaProvider implements MobileMoneyProvider { async getStatus(transactionId: string): Promise { const result = await queryStkStatus(transactionId) - if (result.ResponseCode !== '0') { + if (String(result.ResponseCode) !== '0') { return 'PENDING' } - const status = mapResultCode(result.ResultCode) + const status = mapResultCode(String(result.ResultCode)) if (status === 'CANCELLED') { throw new MobileMoneyError('CANCELLED', result.ResultDesc) diff --git a/lib/payments/mtn-momo.ts b/lib/payments/mtn-momo.ts index 131897d..de70425 100644 --- a/lib/payments/mtn-momo.ts +++ b/lib/payments/mtn-momo.ts @@ -41,6 +41,11 @@ interface TokenCache { let tokenCache: TokenCache | null = null +/** Clears cached OAuth token (for tests). */ +export function resetMtnMomoTokenCacheForTests(): void { + tokenCache = null +} + async function fetchAccessToken(): Promise { const now = Date.now() if (tokenCache && tokenCache.expiresAt > now + 30_000) { From 74e713c548fe9f8b62780607ac7fc524a06e4335 Mon Sep 17 00:00:00 2001 From: emmanard Date: Mon, 1 Jun 2026 10:25:36 +0100 Subject: [PATCH 2/2] feat(wallet-modal): enhance mobile experience with drawer component and refactor modal views - Introduced a responsive drawer for mobile devices in the WalletModal component. - Refactored modal views into a separate WalletModalViews component for better organization. - Added WalletModalHeader for consistent header rendering across different modal variants. - Improved code structure and readability by utilizing TypeScript types for props. --- components/Wallet/WalletModal.tsx | 276 +++++++++++++++++++++--------- hooks/use-is-mobile.ts | 17 ++ 2 files changed, 216 insertions(+), 77 deletions(-) create mode 100644 hooks/use-is-mobile.ts diff --git a/components/Wallet/WalletModal.tsx b/components/Wallet/WalletModal.tsx index b66387c..46d6908 100644 --- a/components/Wallet/WalletModal.tsx +++ b/components/Wallet/WalletModal.tsx @@ -1,6 +1,7 @@ 'use client' import { useMemo, useState } from 'react' +import type { ReactNode } from 'react' import { motion, AnimatePresence } from 'framer-motion' import { Dialog, @@ -9,7 +10,9 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' +import { Drawer } from 'vaul' import { Button } from '@/components/ui/button' +import { cn } from '@/lib/utils' import { Wallet, ExternalLink, @@ -19,8 +22,10 @@ import { RefreshCw, Smartphone, Monitor, + X, } from 'lucide-react' import { useWallet } from '@/hooks/useWallet' +import { useIsMobile } from '@/hooks/use-is-mobile' interface WalletModalProps { open: boolean @@ -28,6 +33,7 @@ interface WalletModalProps { } type ModalView = 'connect' | 'installing' | 'connecting' | 'network-warning' +type ModalVariant = 'dialog' | 'sheet' export function WalletModal({ open, onOpenChange }: WalletModalProps) { const { @@ -41,6 +47,7 @@ export function WalletModal({ open, onOpenChange }: WalletModalProps) { clearError, } = useWallet() + const isMobile = useIsMobile() const [manualView, setManualView] = useState(null) const derivedView = useMemo(() => { @@ -78,54 +85,168 @@ export function WalletModal({ open, onOpenChange }: WalletModalProps) { setManualView('connect') } + const viewsProps = { + view, + onCheckAgain: () => setManualView('connect'), + onConnect: handleConnect, + hasError, + error, + onRetry: handleRetry, + isFreighterInstalled, + onShowInstall: () => setManualView('installing'), + network, + onContinue: () => onOpenChange(false), + onRefresh: handleRetry, + } + + if (isMobile) { + return ( + + + + + + + + +
+ +
+
+
+
+ ) + } + return ( - - {view === 'installing' && ( - setManualView('connect')} /> - )} - {view === 'connect' && ( - setManualView('installing')} - /> - )} - {view === 'connecting' && } - {view === 'network-warning' && ( - onOpenChange(false)} - onRefresh={handleRetry} - /> - )} - + ) } -function InstallView({ onCheckAgain }: { onCheckAgain: () => void }) { +interface WalletModalViewsProps { + view: ModalView + variant: ModalVariant + onCheckAgain: () => void + onConnect: () => void + hasError: boolean + error: string | null + onRetry: () => void + isFreighterInstalled: boolean + onShowInstall: () => void + network: string | null + onContinue: () => void + onRefresh: () => void +} + +function WalletModalViews({ + view, + variant, + onCheckAgain, + onConnect, + hasError, + error, + onRetry, + isFreighterInstalled, + onShowInstall, + network, + onContinue, + onRefresh, +}: WalletModalViewsProps) { + return ( + + {view === 'installing' && ( + + )} + {view === 'connect' && ( + + )} + {view === 'connecting' && } + {view === 'network-warning' && ( + + )} + + ) +} + +function WalletModalHeader({ + icon, + title, + description, + titleClassName, + variant, +}: { + icon: ReactNode + title: string + description: ReactNode + titleClassName?: string + variant: ModalVariant +}) { + if (variant === 'dialog') { + return ( + + + {icon} + {title} + + {description} + + ) + } + return ( +
+

+ {icon} + {title} +

+

{description}

+
+ ) +} + +function InstallView({ + onCheckAgain, + variant, +}: { + onCheckAgain: () => void + variant: ModalVariant +}) { return ( - - - - Install Freighter - - Freighter is required to connect to AFRAMP - + } + title="Install Freighter" + description="Freighter is required to connect to AFRAMP" + />
@@ -143,28 +264,28 @@ function InstallView({ onCheckAgain }: { onCheckAgain: () => void }) { href="https://chrome.google.com/webstore/detail/freighter/bcacfldlkkdogcmkkibnjlakofdplcbk" target="_blank" rel="noopener noreferrer" - className="flex items-center gap-3 p-4 rounded-lg border border-border bg-card hover:bg-muted/50 transition-colors" + className="flex items-center gap-3 p-4 min-h-[56px] rounded-lg border border-border bg-card hover:bg-muted/50 active:bg-muted/70 transition-colors" > - +
Chrome Extension

For Chrome, Brave, Edge browsers

- + - +
Mobile App

iOS & Android available

- +
@@ -178,9 +299,7 @@ function InstallView({ onCheckAgain }: { onCheckAgain: () => void }) {