Welcome to the Merchant Payout Challenge! This is a mobile frontend coding challenge designed to assess your ability to implement a financial payout experience using React Native and Expo.
Your task is to build a merchant dashboard and payout flow that allows users to:
- Review account balances and recent activity with pagination
- Initiate and validate a payout to a bank account with confirmation
- Integrate native device identity for payout requests
- Require biometric authentication for payouts over Β£1,000.00
- Protect the payout screen from screenshots
- Handle various edge cases, including network errors and insufficient funds
The project comes with the following pre-configured technologies:
- Expo (SDK 52+) - Framework for React Native
- TypeScript - For type safety
- Expo Router - File-based routing
- Jest & React Native Testing Library - For testing
The project uses MSW (Mock Service Worker) to mock the banking API. The base URL is configured in constants/index.ts.
Important: MSW intercepts HTTP requests at the network level, which means intercepted requests will not appear in browser DevTools Network tab. Instead, all intercepted requests are logged to the browser console with the
[MSW]prefix, showing the method, URL, status code, and response data. This is expected behavior and allows you to debug API calls through the console.
| Endpoint | Method | Description |
|---|---|---|
/api/merchant |
GET |
Returns available_balance, pending_balance, currency, and activity (list of recent transactions). |
/api/merchant/activity |
GET |
Returns paginated activity items using cursor-based pagination. Query parameters: cursor (optional, activity ID from previous page) and limit (optional, default: 15). Returns { items, next_cursor, has_more }. |
/api/payouts |
POST |
Initiates a payout. Request body: { amount, currency, iban, device_id? } |
Note: The API returns and expects all monetary amounts in the lowest denomination of the currency (e.g., pence for GBP, cents for EUR). For example,
500000represents5000.00 GBPor5000.00 EUR. Amounts can include fractional values (e.g.,99999pence =999.99 GBP).
The types for the API responses are configured in types/api.ts.
The mock API supports specific triggers to test your error handling:
- Service Unavailable:
POST /api/payoutswith an amount of999.99(99999 pence) returns a503 Service Unavailable. - Insufficient Funds:
POST /api/payoutswith an amount of888.88(88888 pence) returns a400 Bad Request.
Your solution will be evaluated based on:
- π§Ή Clean, maintainable code
- π Proper TypeScript usage
- ποΈ Well-structured components
- π― Efficient state management
- Start with Step 1 and work through each step incrementally
- Keep accessibility in mind throughout development
- Use TypeScript effectively
- Test with the provided invalid input values to verify error handling
- Don't hesitate to install additional packages if they help you solve the problem more efficiently
- Consider using libraries for state management, form handling, or UI components if they improve your solution
Goal: Fetch and display the merchantβs financial overview.
Requirements:
- Fetch balance data using the provided API client.
- Display an account balance section showing the merchant's available balance and pending balance with the currency symbol from the API response.
- Display a list of the 3 most recent activity items in a single-row layout showing only the description and amount.
- Display a "show more" button that opens a modal with a full list of activity items.
- Handle loading and error states gracefully.
Goal: Display recent activity with enhanced functionality.
Requirements:
- Display the list of all activity items with type, description, amount, and date (formatted as
DD MM YYYY). - Implement "Infinite Scroll" functionality on the transaction list modal. Load more items automatically as the user scrolls to the bottom.
- Use cursor-based pagination to fetch additional activity items.
- Handle loading and error states gracefully.
Goal: Create a screen for users to send a payout to a bank account with confirmation modal.
Requirements:
- Use a numeric input field for the payout amount.
- Use a dropdown to select the currency (
GBPorEUR). The currency can be different from the merchant's account currency. - Capture the destination IBAN (e.g.,
FR1212345123451234567A12310131231231231). - Ensure the form remains usable when the keyboard is visible.
- Ensure the "Confirm" button is disabled if the input is empty, zero, or negative.
- Display a confirmation screen summarizing the transaction before execution (as shown in the reference images).
- Handle success response by showing Payout confirmation with amount and currency.
- Handle failures (e.g.,
4xx,5xxerrors, insufficient funds) and network errors.
Goal: Identify the Merchant's device identifier using a Native Bridge and send as part of the Payout API request.
Requirements:
- Create a native module named
ScreenSecurity(this module will be extended in Steps 5 and 6). - Function: Implement a
getDeviceId()function in theScreenSecuritynative module that returns a unique device identifier. - Send: Send this ID with the Payout request as
device_id.
π± Reference Screenshots
| iOS | Android |
|---|---|
| (No visual changes - device ID is sent in the background) | (No visual changes - device ID is sent in the background) |
Goal: Secure payouts over Β£1,000.00 (or equivalent in selected currency, e.g., β¬1,000.00 for EUR) using a custom native bridge (no 3rd party libs).
Requirements:
- Bridge to Native: Extend the existing
ScreenSecuritynative module created in Step 4. - AsyncFunction: The module should expose a
isBiometricAuthenticated()AsyncFunction for biometrics. - Before the
/api/payoutscall, check if the payout amount exceeds the threshold (1,000.00in the selected currency). If it does, await the native bridge. If the promise resolvesfalse, abort the payout. - If biometrics are not setup, inform the user to setup biometrics in the settings and abort the payout.
Simulator testing:
- iOS: In Simulator menu, go to
Features>Face ID>Enrolled. Then trigger your payout and selectFeatures>Face ID>Matching Face. - Android: Go to Emulated devices
Settings>Security>Fingerprintor searchfingerprintin the search bar for the Settings screen and enable Authentication. Then in the Simuulator you can use the Extended Controls(...)>Fingerprintto simulate a touch.
Goal: Make sure the Merchant is aware of the risk of screenshots on the Payout screen.
Requirements:
- Bridge to Native: Extend the existing
ScreenSecuritynative module created in Step 4. - Emit Native Events: The module should listen for the system's "Screenshot" event and emit an
onScreenshotTakenevent to the JS layer. - iOS: Use
UIApplication.userDidTakeScreenshotNotification. - Android (API 34+): Use
Activity.ScreenCaptureCallback. - UI Reaction: On the Payout screen, listen for this event and show a non-intrusive warning (like a Toast or an Alert) reminding the user to keep their financial data private.
Simulator testing:
-
iOS: Use Device β Trigger Screenshot from the Simulator menu
Note:
Cmd + Sdoes not trigger the notification in the simulator. -
Android: The Android
14+API requires hardware button presses. In an emulator, you can simulate this using the emulator's Power and Volume Down buttons.You can use the
adbcommand to trigger the screenshot warning on the simulator:adb shell input keyevent 120 # Power + Volume DownNote: This API only detects hardware button presses (Power + Volume Down), not
adbscreencap or emulator screenshot buttons.
Good luck! We are excited to see how you build this experience. π





















