This file provides guidance to AI agents when working with code in this repository.
WPay Mobile POS is a React Native point-of-sale application that enables merchants to accept cryptocurrency payments via WalletConnect. The app allows merchants to:
- Generate QR codes for payment requests
- Accept payments through WalletConnect-compatible wallets
- Print thermal receipts for completed transactions
- Manage merchant settings and configurations
- Support multiple branded variants (white-labeling)
The app is built with Expo and React Native, supporting Android, iOS, and Web platforms.
- React Native: 0.81.5
- Expo: ^54.0.23 (with Expo Router for navigation)
- TypeScript: ~5.9.2
- React: 19.1.0
- @tanstack/react-query: Data fetching and caching
- zustand: State management (lightweight alternative to Redux)
- react-hook-form: Form handling
- expo-router: File-based routing
- react-native-thermal-pos-printer: Thermal printer integration
- react-native-qrcode-skia: QR code generation
- @shopify/react-native-skia: Graphics rendering
- expo-secure-store: Secure credential storage
- react-native-mmkv: Fast key-value storage
- @sentry/react-native: Error tracking and monitoring
- ESLint: Code linting
- Prettier: Code formatting
- Jest: Testing framework
- patch-package: Library patching for custom fixes
pos-app/
├── app/ # Expo Router screens (file-based routing)
│ ├── index.tsx # Home screen
│ ├── amount.tsx # Amount input screen
│ ├── scan.tsx # QR code display & payment polling
│ ├── payment-success.tsx # Success screen with receipt printing
│ ├── payment-failure.tsx # Failure screen
│ ├── settings.tsx # Settings & configuration
│ ├── activity.tsx # Transaction history screen
│ └── logs.tsx # Debug logs viewer
├── components/ # Reusable UI components
├── constants/ # Theme, variants, spacing, etc.
├── hooks/ # Custom React hooks
├── services/ # API client and payment services
├── store/ # Zustand state stores
├── utils/ # Utility functions
└── assets/ # Images, fonts, icons
The app uses Zustand for state management with two main stores:
-
useSettingsStore(store/useSettingsStore.ts)- Merchant ID and API key
- Theme mode (light/dark)
- Selected variant
- Device ID
- Biometric authentication settings
- Printer connection status
- Transaction filter preference (for Activity screen)
-
useLogsStore(store/useLogsStore.ts)- Debug logs for troubleshooting
- Log levels: info, warning, error
Uses Expo Router with file-based routing:
- Routes are defined by file structure in
app/directory - Navigation via
router.push(),router.replace(),router.dismiss() - Type-safe routing with TypeScript
-
Home Screen (
app/index.tsx)- "New sale" button to start payment
- "Activity" button to view transaction history
- "Settings" button for configuration
- Validates merchant setup before allowing payments
-
Amount Input (
app/amount.tsx)- Custom numeric keyboard component
- Amount formatting (always 2 decimal places)
- Form validation with react-hook-form
-
QR Code Display (
app/scan.tsx)- Generates payment request via API
- Displays QR code for wallet scanning
- Polls payment status every 2 seconds
- Handles payment success/failure navigation
- Shows WalletConnect loading animation
-
Payment Success (
app/payment-success.tsx)- Animated expanding circle background
- Displays payment details
- Option to print receipt
- "New Payment" button to start over
-
Payment Failure (
app/payment-failure.tsx)- Displays error information
- Allows retry or return to home
-
Activity Screen (
app/activity.tsx)- Transaction history list with pull-to-refresh
- Filter tabs: All, Failed, Pending, Completed
- Transaction detail modal on tap
- Empty state when no transactions
- Uses Merchant Portal API for data fetching
- Thermal Printer Support (
utils/printer.ts)- Bluetooth/USB printer connection
- Receipt generation with:
- Variant-specific logo (base64 encoded)
- Transaction ID, date, payment method
- Amount in USD
- Token symbol and amount (if applicable)
- Network name
- Automatic paper cutting after print
- Error handling and logging
- Merchant Setup (
app/settings.tsx)- Merchant ID input
- API key configuration (stored securely)
- Device ID generation/management
- Variant selection dropdown
- Theme mode toggle (light/dark)
- Biometric authentication toggle
- Printer connection testing
- Test receipt printing
- App version display
- Logs viewer access
- Secure Storage: API keys stored in
expo-secure-store - Biometric Authentication: Face ID / Touch ID support
- PIN Protection: Optional PIN modal for sensitive actions
- Secure Credentials: Never logged or exposed
- Light/Dark Mode: System-aware theme switching
- Variant Support: Multiple branded variants (see Variants System section)
- Dynamic Colors: Theme colors adapt based on variant selection
- Accessibility: Proper contrast ratios maintained
- Base URL from
EXPO_PUBLIC_API_URLenvironment variable - Request/response interceptors
- Error handling
startPayment(request)
- Creates new payment request
- Requires merchant ID and API key
- Returns payment ID and QR code URI
getPaymentStatus(paymentId)
- Polls payment status
- Returns payment state (pending, completed, failed)
- Includes transaction details when completed
All Payment API requests include:
Api-Key: Merchant API keyMerchant-Id: Merchant identifierSdk-Name: "pos-device"Sdk-Version: "1.0.0"Sdk-Platform: "react-native"
The Merchant Portal API is a separate backend used for fetching transaction history (Activity screen).
- Base URL from
EXPO_PUBLIC_MERCHANT_API_URLenvironment variable - Generic HTTP client (no credentials baked in)
- API key passed per-request via headers
- Used by native apps (iOS/Android) for direct API calls
- Vercel serverless function that proxies requests to the Merchant Portal API (web only)
- API key read from server-side env (
EXPO_PUBLIC_MERCHANT_PORTAL_API_KEY) - Client only sends
x-merchant-idheader - Avoids CORS issues by making requests server-side
getTransactions(options)
- Fetches merchant transaction history
- Endpoint:
GET /merchants/{merchant_id}/payments - Supports filtering by status, date range, pagination
- Returns array of
PaymentRecordobjects
import { useTransactions } from "@/services/hooks";
const { data, isLoading, isError, refetch } = useTransactions({
filter: "all", // "all" | "completed" | "pending" | "failed"
enabled: true,
});- React Query hook with built-in caching (5 min stale time, 30 min cache)
- Automatic retry on failure (2 retries)
- Client-side filtering via
filteroption - Logs errors to
useLogsStorefor debugging
Required environment variables (.env):
EXPO_PUBLIC_PROJECT_ID="" # WalletConnect project ID
EXPO_PUBLIC_SENTRY_DSN="" # Sentry error tracking DSN
SENTRY_AUTH_TOKEN="" # Sentry authentication token
EXPO_PUBLIC_API_URL="" # Payment API base URL
EXPO_PUBLIC_GATEWAY_URL="" # WalletConnect gateway URL
EXPO_PUBLIC_DEFAULT_MERCHANT_ID="" # Default merchant ID (optional)
EXPO_PUBLIC_DEFAULT_PARTNER_API_KEY="" # Default partner API key (optional)
EXPO_PUBLIC_MERCHANT_API_URL="" # Merchant Portal API base URL
EXPO_PUBLIC_MERCHANT_PORTAL_API_KEY="" # Merchant Portal API key (for Activity screen)Copy .env.example to .env and fill in values.
- Node.js (LTS version recommended)
- Android Studio (for Android development)
- Xcode (for iOS development on macOS)
- Expo CLI
This project uses npm (not pnpm or yarn). Always use npm commands for installing dependencies and running scripts.
-
Install dependencies
npm install
-
Set up environment variables
cp .env.example .env # Edit .env with your values -
Create native folders
npm run prebuild
-
Start development server
npm run android # Android npm run ios # iOS npm run web # Web
npm start: Start Expo dev servernpm run android: Run on Androidnpm run ios: Run on iOSnpm run web: Run on webnpm run android:build: Build Android release APKnpm run lint: Run ESLintnpm test: Run Jest tests
app/_layout.tsx: Root layout with navigation setupapp/index.tsx: Home screen entry pointapp/amount.tsx: Payment amount inputapp/scan.tsx: QR code display and payment pollingapp/payment-success.tsx: Success screen with animationsapp/payment-failure.tsx: Error handling screenapp/settings.tsx: Settings and configuration
services/client.ts: Payment API client configurationservices/merchant-client.ts: Merchant Portal API client (native)services/payment.ts: Payment API functionsservices/transactions.ts: Transaction fetching (native: direct API)services/transactions.web.ts: Transaction fetching (web: server-side proxy)services/hooks.ts: React Query hooks for API calls (includinguseTransactions)api/payment.ts: Vercel serverless function for payment creation (web)api/payment-status.ts: Vercel serverless function for payment status (web)api/transactions.ts: Vercel serverless function for transaction list (web)
utils/printer.ts: Thermal printer integrationutils/currency.ts: Currency formatting utilitiesutils/misc.ts: Date formatting and helpersutils/navigation.ts: Navigation helpersutils/secure-storage.ts: Secure storage wrapperutils/biometrics.ts: Biometric authentication helpers
store/useSettingsStore.ts: App settings and configurationstore/useLogsStore.ts: Debug logging store
constants/theme.ts: Base theme color definitionsconstants/variants.ts: Variant configurationsconstants/printer-logos.ts: Base64-encoded printer logosconstants/spacing.ts: Spacing scale constants
components/qr-code.tsx: QR code display componentcomponents/numeric-keyboard.tsx: Custom numeric inputcomponents/pin-modal.tsx: PIN entry modalcomponents/button.tsx: Themed button componentcomponents/themed-text.tsx: Theme-aware text componentcomponents/status-badge.tsx: Transaction status badge (Completed/Pending/Failed)components/transaction-card.tsx: Transaction list itemcomponents/filter-tabs.tsx: Filter tabs for Activity screencomponents/transaction-detail-modal.tsx: Transaction detail bottom sheetcomponents/empty-state.tsx: Reusable empty state component
This POS app supports a variants system that allows for minor UI customizations while maintaining the same core functionality. Variants enable white-labeling and branding customization for different clients or use cases.
-
Theme System (
constants/theme.ts)- Defines base color palette for light and dark modes
- Provides default colors used across the app
- Colors can be overridden by variants
-
Variants Configuration (
constants/variants.ts)- Defines available variants and their customizations
- Each variant can override theme colors, logos, and default theme mode
- Variants are selected via settings and stored in Zustand store
-
Printer Logos (
constants/printer-logos.ts)- Contains base64-encoded logos for thermal printer receipts
- Each variant has its own printer logo
- Default logo uses
brand.pngconverted to base64
Each variant is defined with:
- name: Display name (e.g., "Solflare", "Binance")
- brandLogo: Image asset for UI branding (loaded via
require()) - brandLogoWidth: Optional width override for brand logo
- printerLogo: Base64-encoded string for receipt printing
- defaultTheme: Optional default theme mode ("light" or "dark")
- colors: Color overrides for light and dark themes
Variants can override any color from the base theme:
- Colors are merged with base theme colors
- Only specified colors are overridden; others use defaults
- Both light and dark theme overrides are supported
solflare: {
name: "Solflare",
brandLogo: require("@/assets/images/variants/solflare_brand.png"),
printerLogo: SOLFLARE_LOGO_BASE64,
defaultTheme: "dark",
colors: {
light: {
"icon-accent-primary": "#FFEF46",
"bg-accent-primary": "#FFEF46",
"bg-payment-success": "#FFEF46",
"text-payment-success": "#202020",
"border-payment-success": "#363636",
"text-invert": "#202020",
},
dark: {
// Similar overrides for dark theme
},
},
}- default: Base variant with blue accent colors (#0988F0)
- solflare: Yellow/gold branding (#FFEF46)
- binance: Yellow branding (#FCD533)
- phantom: Purple branding (#AB9FF2)
- solana: Purple branding (#9945FF)
Commonly overridden colors in variants:
bg-accent-primary: Primary accent backgroundbg-payment-success: Payment success screen backgroundicon-accent-primary: Accent icon colortext-payment-success: Text color on success screenborder-payment-success: Border color for success elementstext-invert: Inverted text (for dark backgrounds)
import { useTheme } from "@/hooks/use-theme-color";
const Theme = useTheme();
// Theme["bg-payment-success"] will use variant override if setVariants are stored in Zustand store (store/useSettingsStore.ts):
- Selected variant persists across app sessions
- Can be changed in Settings screen
- Affects all themed components immediately
-
Add variant logo image
- Place in
assets/images/variants/<variant-name>_brand.png - PNG format recommended
- Place in
-
Convert logo to base64 for printer
- Use online tool or command:
base64 -i assets/images/variants/<variant-name>_brand.png - Add to
constants/printer-logos.tsasexport const <VARIANT>_LOGO_BASE64
- Use online tool or command:
-
Define variant in
constants/variants.ts- Add variant name to
VariantNametype - Import printer logo base64
- Add variant configuration to
Variantsobject - Specify color overrides for light/dark themes
- Add variant name to
-
Update version code (if needed)
- Increment
expo.android.versionCodeinapp.json
- Increment
// 1. In printer-logos.ts
export const MYVARIANT_LOGO_BASE64 = "data:image/png;base64,...";
// 2. In variants.ts
import { MYVARIANT_LOGO_BASE64 } from "./printer-logos";
export type VariantName =
| "default"
| "solflare"
| "binance"
| "phantom"
| "solana"
| "myvariant"; // Add here
export const Variants: Record<VariantName, Variant> = {
// ... existing variants
myvariant: {
name: "My Variant",
brandLogo: require("@/assets/images/variants/myvariant_brand.png"),
printerLogo: MYVARIANT_LOGO_BASE64,
defaultTheme: "light",
colors: {
light: {
"bg-accent-primary": "#CUSTOM_COLOR",
"bg-payment-success": "#CUSTOM_COLOR",
// ... other overrides
},
dark: {
// ... dark theme overrides
},
},
},
};-
Color Contrast: When overriding colors, ensure sufficient contrast for accessibility
- Light backgrounds need dark text
- Dark backgrounds need light text
- Some variants use
text-invertoverride for better contrast
-
Printer Logos: Must be base64-encoded PNG strings
- Format:
"data:image/png;base64,<base64-string>" - Used in thermal printer receipts
- Logo size is automatically handled by printer library
- Format:
-
Default Theme: Variants can specify a default theme mode
- Users can still switch themes manually
- Default applies on first launch
-
Payment Success Color: The
bg-payment-successcolor is used for:- Payment success screen background (expanding circle animation)
- Success screen buttons
- Success screen text (via
text-payment-success)
-
Variant Persistence: Selected variant is stored in Zustand store
- Persists across app restarts
- Can be changed in Settings screen
- Open Settings screen
- Select different variants from dropdown
- Verify:
- Brand logo changes in header
- Accent colors update throughout app
- Payment success screen uses variant colors
- Receipt printing uses variant logo
constants/theme.ts: Base theme colorsconstants/variants.ts: Variant definitionsconstants/printer-logos.ts: Printer logo base64 stringsstore/useSettingsStore.ts: Variant selection stateapp/settings.tsx: Variant selection UIhooks/use-theme-color.ts: Theme color hook with variant support
-
Required Files (get from mobile team or 1Password):
android/secrets.propertiesandroid/app/wc_rn_upload.keystore
-
Build Release APK:
npm run android:build
Output:
android/app/build/outputs/apk/release/app-release.apk -
Install via USB:
adb devices # Get device ID adb -s <DEVICE_ID> install android/app/build/outputs/apk/release/app-release.apk
app.json.
- Increment version code: Update
expo.android.versionCodeinapp.jsonfor each change - Current version code: Check the current value in
app.jsonand increment by 1 - Why: Android requires a unique version code for each release. Without incrementing, new builds cannot be installed over previous versions
- Example: If current version code is
15, change it to16for your changes - Current version code: 16
- @tanstack/react-query: Manages API calls, caching, and polling for payment status
- zustand: Lightweight state management for settings and logs
- expo-router: File-based routing system
- react-native-thermal-pos-printer: Bluetooth/USB thermal printer integration
- react-native-qrcode-skia: QR code generation for payment requests
- expo-secure-store: Secure storage for API keys and sensitive data
- react-native-mmkv: Fast key-value storage for non-sensitive data
- expo-local-authentication: Biometric authentication (Face ID/Touch ID)
- @sentry/react-native: Error tracking and crash reporting
- react-hook-form: Form handling and validation
- react-native-reanimated: Animations (used in payment success screen)
import { useTheme } from "@/hooks/use-theme-color";
const Theme = useTheme();
// Access colors: Theme["bg-accent-primary"]import { router } from "expo-router";
// Navigate to screen
router.push("/amount");
// Navigate with params
router.push({
pathname: "/scan",
params: { amount: "10.00" },
});
// Replace current screen
router.replace("/payment-success");
// Dismiss modal
router.dismiss();import { usePaymentStatus } from "@/services/hooks";
const { data, isLoading, error } = usePaymentStatus(paymentId, {
enabled: !!paymentId,
refetchInterval: 2000, // Poll every 2 seconds
});import { secureStorage, SECURE_STORAGE_KEYS } from "@/utils/secure-storage";
// Store
await secureStorage.setItem(SECURE_STORAGE_KEYS.PARTNER_API_KEY, apiKey);
// Retrieve
const apiKey = await secureStorage.getItem(SECURE_STORAGE_KEYS.PARTNER_API_KEY);console.log() statements in production code.
-
Use the logging system: For debugging, use the app's built-in logging system via
useLogsStore:import { useLogsStore } from "@/store/useLogsStore"; const addLog = useLogsStore((state) => state.addLog); addLog("info", "Payment completed", "payment-success", "handlePrintReceipt");
-
Remove console.logs before committing: Always remove any
console.log(),console.error(), or other console statements before committing code. -
View logs in app: Users can view logs in the Settings screen → View Logs
-
Production builds: Console statements can impact performance and expose sensitive information in production builds.
Always run these checks and fix any errors before committing:
npm run lint # Check and fix ESLint errors
npx prettier --write . # Format code with Prettier
npx tsc --noEmit # Check for TypeScript errorsFix any errors found. Pre-existing TypeScript errors in unrelated files can be ignored.
- Follow TypeScript best practices
- Use ESLint and Prettier for consistent formatting
- Prefer functional components with hooks
- Use TypeScript types/interfaces for all props and data structures
- No trailing whitespace
- Check Bluetooth permissions in Android settings
- Verify printer is paired and connected
- Check logs in Settings → View Logs
- Test connection via Settings → Test Printer Connection
- Verify merchant ID and API key in Settings
- Check network connectivity
- Review logs for API errors
- Ensure
EXPO_PUBLIC_API_URLis correctly configured
- Run
npm run prebuildafter dependency changes - Clear Metro cache:
npx expo start --clear - Clean Android build:
cd android && ./gradlew clean
When the app is viewed on desktop web browsers, it renders inside a simulated POS device frame to provide a realistic preview of the mobile experience. This system handles frame rendering, scaling, and modal positioning.
-
Desktop Frame Wrapper (
components/desktop-frame-wrapper.web.tsx)- Wraps the entire app in a device frame on desktop web
- Detects desktop vs mobile web using
useIsDesktopWebhook - Auto-scales the frame to fit the browser window
- Provides modal portal context for rendering modals inside the frame
- On mobile web or native, renders children unchanged (no frame)
-
Desktop Frame Constants (
constants/desktop-frame.ts)- Defines device dimensions (width, height)
- Bezel styling (width, color, radius)
- Screen radius for rounded corners
- Background colors for light/dark themes
- Box shadow for depth effect
-
useIsDesktopWeb Hook (
hooks/use-is-desktop-web.ts)- Returns
truewhen running on desktop web (window width > 768px) - Returns
falseon mobile web or native platforms - Listens for window resize events to update dynamically
- Returns
The desktop frame is applied in index.web.tsx:
import { DesktopFrameWrapper } from "@/components/desktop-frame-wrapper.web";
function WrappedApp() {
return (
<DesktopFrameWrapper>
<App />
</DesktopFrameWrapper>
);
}React Native's <Modal> component renders at the viewport level with fixed positioning, which causes modals to appear outside the device frame on desktop web. To solve this, a portal system renders modals inside the frame.
-
Modal Portal Context (
components/modal-portal-context.tsx)- Provides a ref to the modal container element
- Used by web modals to render via
createPortal
-
FramedModal (
components/framed-modal.tsx/framed-modal.web.tsx)- Platform-specific modal wrapper
- Native (
framed-modal.tsx): Uses React Native's<Modal>directly - Web (
framed-modal.web.tsx): UsescreatePortalto render inside the frame container
Replace <Modal> with <FramedModal> for modals that should appear inside the device frame:
import { FramedModal } from "./framed-modal";
function MyModal({ visible, onClose, children }) {
return (
<FramedModal visible={visible} onRequestClose={onClose}>
{/* Modal content - include your own overlay and container */}
<Pressable style={styles.overlay} onPress={onClose}>
<View style={styles.container}>
{children}
</View>
</Pressable>
</FramedModal>
);
}DesktopFrameWrappercreates a container div withref={modalContainerRef}ModalPortalProvidermakes this ref available via contextFramedModal.web.tsxusesuseModalPortal()to get the container ref- When visible, it renders children via
createPortal(content, containerRef.current) - This positions the modal inside the frame instead of at viewport level
The frame automatically scales to fit the browser window:
- Calculates available height (window height minus label)
- Computes scale factor:
Math.min(1, availableHeight / totalFrameHeight) - Applies CSS transform:
transform: scale(${scale}) - Maintains aspect ratio and centers the frame
The frame adapts to light/dark mode:
- Background color changes based on color scheme
- Screen background matches app theme
- Bezel color remains constant (device hardware appearance)
index.web.tsx: Web entry point with DesktopFrameWrappercomponents/desktop-frame-wrapper.web.tsx: Frame wrapper componentcomponents/modal-portal-context.tsx: Modal portal context providercomponents/framed-modal.tsx: Native modal wrappercomponents/framed-modal.web.tsx: Web modal with portal supportconstants/desktop-frame.ts: Frame dimension constantshooks/use-is-desktop-web.ts: Desktop detection hook
- Platform-specific files: The
.web.tsxsuffix ensures the web version is used only on web platform - Modal children:
FramedModalonly provides the container; children must include their own overlay and content styling - Escape key:
FramedModal.webhandles Escape key to close modals - Mobile web fallback: If no portal container exists (mobile web), the modal renders in place with absolute positioning
- README.md: Setup and development instructions
- app.json: Expo configuration
- package.json: Dependencies and scripts
- tsconfig.json: TypeScript configuration