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
158 changes: 59 additions & 99 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,162 +1,122 @@
# Welcome to your Expo app 👋

# Ding Payments — Mobile Client MVP

This repository contains the core React Native application built with Expo Router and the Soroban Smart Contract SDK for the Ding Payments network.
Peer-to-peer contactless (NFC) payments on Stellar with a self-custodial wallet and passkey authentication.

## 🛠 Prerequisites
## Prerequisites

- **Node.js**: v18 or later
- **Package Manager**: `npm`
- **Development Target**: Physical iOS or Android device (required for NFC, passkeys, and SecureStore biometrics); simulator/emulator for UI-only work
- **Node.js** v20+
- **npm**
- **Xcode** (iOS) or **Android Studio** (Android)
- Physical NFC devices for end-to-end NFC validation

> ⚠️ Native Framework Limitation: This application leverages advanced hardware integrations including NFC capabilities and Passkey WebAuthn modules. These features cannot execute inside standard Expo Go. You must use an Expo Development Build (EAS Development Build) to validate NFC, passkeys, and SecureStore functionality on physical devices.
> **Expo Go is not supported.** NFC, passkeys, and SecureStore require a **development build** (`npx expo run:ios` or `npx expo run:android`, or EAS dev build).

## 🚀 Local Development Setup
## Setup

1. **Clone the Repository & Fetch Dependencies**
1. Clone and install dependencies:

```bash
npm install
cp .env.example .env
```

2. **Build requirements for native features**
- NFC and passkey research require an Expo development build or custom native runtime.
2. Build requirements for native features:

- Run `npx expo prebuild` and `npx expo run:android` / `npx expo run:ios` for device validation.
- Use `npm run dev-client` to launch a dev-client session after native dependencies are installed.

## EAS Development Build

1. **Install and authenticate EAS CLI**
1. Install and authenticate EAS CLI:

```bash
npm install -g eas-cli
eas login
```

On first setup, link the project with `eas init`. Build profiles live in `eas.json` (`development`, `preview`, `production`).

2. **Create a development build**
2. Create a development build:

```bash
npm run dev:build:android
# or
npm run dev:build:ios
```

Install the resulting build on a **physical device** (`.apk` on Android; iOS via internal distribution or TestFlight).

3. **Start the dev client**
3. Start the dev client:

```bash
npm run dev-client
```

This runs `expo start --dev-client` and connects the installed development build to Metro.

4. **Native rebuild required**
Rebuild and reinstall the development build after changes to:
- `app.config.ts` plugins, permissions, or entitlements
- native dependencies (for example `expo-dev-client`, `expo-secure-store`, `react-native-nfc-manager`, `react-native-passkey`)

JavaScript-only changes do not require a native rebuild.

5. **Expo Go limitations**
Do not use Expo Go to validate NFC, passkeys, or SecureStore with biometric authentication. These flows require a development build with `expo-dev-client`.
4. **Native rebuild required** after changes to `app.config.ts` plugins, permissions, or native dependencies (`expo-dev-client`, `expo-secure-store`, `react-native-nfc-manager`, `react-native-passkey`).

6. **Simulator vs physical device**
## Quality checks (CI)

| Feature | Simulator / emulator | Physical device |
| ------------------------ | -------------------- | --------------- |
| General UI / routing | Yes | Yes |
| NFC | No | Yes (required) |
| Passkeys | Limited / unreliable | Yes (required) |
| SecureStore + biometrics | Limited | Yes (required) |

Use a physical device for native capability smoke tests, including the `/c05` spike page.

## ✅ Quality checks (CI)

CI (`.github/workflows/ci-client.yml`) runs the exact same npm scripts you run locally, so a green local run means a green pipeline. Run all three before opening a PR:
Run before opening a PR:

```bash
npm run build # tsc --noEmit — type-checks the project
npm run lint # expo lint (ESLint flat config + Prettier rules)
npm run format # prettier --write . — auto-formats the repo
npm run build
npm run lint
npm run test
npm run format:check
```

Helper scripts:

| Script | Purpose |
| ------------------------------------- | ---------------------------------------------- |
| `npm run build` / `npm run typecheck` | TypeScript type-check (`tsc --noEmit`) |
| `npm run lint` | Report lint problems (`expo lint`) |
| `npm run lint:fix` | Auto-fix lint problems |
| `npm run format` | Format all files with Prettier |
| `npm run format:check` | Verify formatting without writing (used by CI) |

Tooling config lives at the repo root: [`eslint.config.mjs`](eslint.config.mjs) (Expo flat config + Prettier) and [`.prettierrc`](.prettierrc) (`singleQuote`, `trailingComma: es5`). Editors with the ESLint and Prettier extensions pick these up automatically.

## C05 Spike documentation

- Passkey ADR: [`docs/adr-passkey-library.md`](docs/adr-passkey-library.md)
- Stellar ADR: [`docs/adr-stellar-sdk.md`](docs/adr-stellar-sdk.md)
- NFC ADR: [`docs/adr-nfc-library.md`](docs/adr-nfc-library.md)
- Spike PoC page: open `/c05` in the app after starting the dev-client.

This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).

## Get started
## NFC development (C10)

1. Install dependencies
The NFC core stack lives under `src/features/nfc/`:

```bash
npm install
```

2. Start the app

```bash
npx expo start
```
| Module | Purpose |
| ---------------------------------------- | ----------------------------------------------------- |
| `services/NfcService.*` | Native abstraction (support checks, sessions) |
| `schemas/paymentRequest.ts` | Zod schema for `payment_request.v1` payloads |
| `services/NfcPayloadCodec.ts` | Compact JSON encode/decode with size guard |
| `services/NfcWriter.ts` / `NfcReader.ts` | Writer (receiver) and reader (payer) sessions |
| `state/nfcSessionStore.ts` | Session state machine + `nfcActive` lock coordination |
| `services/nfc-spike.ts` | Manual PoC helpers for device verification |

For NFC, passkeys, and SecureStore testing, use `npm run dev-client` (`expo start --dev-client`) with an installed development build—not Expo Go.

In the output, you'll find options to open the app in a

- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo

You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).

## Get a fresh project

When you're ready, run:
### Rebuild after native NFC changes

```bash
npm run reset-project
npx expo prebuild --clean
npx expo run:ios
# or
npx expo run:android
```

This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
### Smoke test on device

```typescript
import { nfcSpikeCheckSupport } from '@/features/nfc/services/nfc-spike';

### Other setup steps
const { supported, enabled } = await nfcSpikeCheckSupport();
```

- To set up ESLint for linting, run `npx expo lint`, or follow our guide on ["Using ESLint and Prettier"](https://docs.expo.dev/guides/using-eslint/)
- If you'd like to set up unit testing, follow our guide on ["Unit Testing with Jest"](https://docs.expo.dev/develop/unit-testing/)
- Learn more about the TypeScript setup in this template in our guide on ["Using TypeScript"](https://docs.expo.dev/guides/typescript/)
See [docs/adr-nfc-library.md](docs/adr-nfc-library.md) for platform constraints and payload limits (880 bytes max).

## Learn more
## Scripts

To learn more about developing your project with Expo, look at the following resources:
| Command | Description |
| ------------------------------------- | -------------------------- |
| `npm start` | Start Expo dev server |
| `npm run dev-client` | Start Expo with dev-client |
| `npm run build` / `npm run typecheck` | TypeScript check |
| `npm test` | Run unit tests |
| `npm run lint` | ESLint via Expo |
| `npm run format:check` | Prettier check (CI) |

- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
## Documentation

## Join the community
- [Product flows & system definition](docs/ding-payments.md)
- [Client MVP build plan](docs/build-plan-client-mvp.md)
- [NFC library ADR](docs/adr-nfc-library.md)

Join our community of developers creating universal apps.
## License

- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
MIT
12 changes: 10 additions & 2 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
infoPlist: {
...(config.ios?.infoPlist ?? {}),
NFCReaderUsageDescription:
'Use NFC to read and write payment requests securely for Ding Payments.',
'Ding Payments uses NFC to share and receive payment requests between devices.',
NSFaceIDUsageDescription: 'Use Face ID to authenticate passkey operations safely.',
},
},
Expand All @@ -31,7 +31,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
backgroundImage: './assets/images/android-icon-background.png',
monochromeImage: './assets/images/android-icon-monochrome.png',
},
permissions: ['NFC'],
permissions: ['android.permission.NFC'],
predictiveBackGestureEnabled: false,
},
web: {
Expand Down Expand Up @@ -59,6 +59,14 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
configureAndroidBackup: true,
},
],
[
'react-native-nfc-manager',
{
nfcPermission:
'Ding Payments uses NFC to share and receive payment requests between devices.',
includeNdefEntitlement: true,
},
],
],
experiments: {
typedRoutes: true,
Expand Down
97 changes: 63 additions & 34 deletions docs/adr-nfc-library.md
Original file line number Diff line number Diff line change
@@ -1,66 +1,95 @@
# ADR: NFC library selection for Expo mobile
# ADR: NFC Library Selection — react-native-nfc-manager

- Status: Accepted
- Date: 2026-06-19
- Related: C05, CLI-045
- **Status:** Accepted
- **Date:** 2026-06-19
- **Related:** C05 (CLI-045), C10 (CLI-046–052)

## Context

The app needs NFC NDEF read/write support for payment request payloads on physical devices. Expo Go cannot be used for this verification, so the runtime must be validated in a dev-client or custom native build.
Ding Payments requires peer-to-peer NFC transport for payment-request payloads on iOS and Android. Expo Go does not expose native NFC APIs; a development build is mandatory.

## Decision

We will use `react-native-nfc-manager` for NFC integration.
Use **react-native-nfc-manager** (v3.17+) with the official Expo config plugin.

### Why this library?

- It is the most mature React Native NFC library with both Android and iOS support.
- It supports NDEF scanning and writing flows needed for payment request roundtrips.
- It is compatible with Expo native builds and supports the required native permissions.
- Mature NDEF read/write APIs on Android and Core NFC on iOS
- Official Expo config plugin for permissions and entitlements
- Active maintenance and broad community usage
- Fits the abstraction layer (`NfcService`) without leaking native details to product code

### Alternatives considered

- `react-native-hce` or similar NFC libraries: considered weaker in NDEF support for both platforms.
- Custom native Objective-C/Java modules: higher maintenance risk and slower validation.
- NFC-only web alternatives: rejected because the client must validate native NFC behavior on devices.
| Option | Rejected because |
|--------|------------------|
| `react-native-hce` or similar | Weaker NDEF support across both platforms |
| Custom native modules | Higher maintenance; slower validation |
| Expo Go only | No NFC access |
| QR-only | Out of scope for C10; planned as fallback |

## Version pinning

- `react-native-nfc-manager@^3.4.0`
- `react-native-nfc-manager@^3.17.2`

## Compatibility matrix
## Platform constraints

- iOS: Core NFC supported on devices with NFC hardware and iOS 13+; iOS only supports NDEF tag discovery for this spike.
- Android: NFC requires `android.permission.NFC`; supported devices must have NFC hardware enabled.
- Expo Go: unsupported for NFC runtime validation.
| Platform | Capability | Notes |
|----------|------------|-------|
| Android | NDEF push + tag reader mode | Primary P2P path via `setNdefPushMessage` |
| iOS | Core NFC reader / NDEF write to tags | P2P limited; validate on physical hardware |
| Web | Not supported | Stub returns `isSupported: false` |
| Expo Go | Not supported | Requires dev build rebuild after native changes |

## Implementation notes
## Payload limits

- App configuration is updated in `app.config.ts` to declare Android NFC permission and iOS NFC usage description.
- The spike implementation is isolated at `src/features/nfc/services/nfc-spike.ts`.
- The roundtrip payload for JSON NDEF should be capped to a safe practical limit, typically under 880 bytes.
- The ADR is validated through the PoC page at `/c05` after running the Expo dev-client.
- **Max NDEF payload:** 880 bytes (conservative; typical Type 2 tag usable ~888 bytes minus overhead)
- **Encoding:** UTF-8 compact JSON (`application/json` MIME NDEF record)
- **Schema:** `payment_request.v1` — see `src/features/nfc/schemas/paymentRequest.ts`

## Permissions

### iOS

- `NFCReaderUsageDescription` in Info.plist (via config plugin)
- NDEF entitlement: `com.apple.developer.nfc.readersession.formats` (via `includeNdefEntitlement: true`)

### Android

- `android.permission.NFC` in AndroidManifest (via config plugin)
- Minimum SDK enforced by plugin (API 31+)

## Session semantics (C10)

| Session | Timeout | Policy |
|---------|---------|--------|
| Writer (receiver) | 60s | Auto-cancel + resource cleanup |
| Reader (payer) | 45s | Single-read per session; ignore duplicates |

## Validation matrix

- Android physical device: NFC initialization and NDEF write/read roundtrip works.
- iOS physical device: NFC initialization and NDEF read/write behave as expected under Core NFC constraints.
- Payload size validation: JSON payload remains below 880 bytes and roundtrip is successful on both test devices.
- Android physical device: NFC initialization and NDEF write/read roundtrip works
- iOS physical device: NFC read/write under Core NFC constraints
- Payload size validation: JSON payload remains below 880 bytes
- PoC page: `/c05` in dev-client for spike flows; C10 services in `src/features/nfc/`

## Rebuild requirement

## Manual validation note
Any change to `app.config.ts` NFC plugin settings requires:

Use the `/c05` test page in the dev-client to execute NFC write/read flows and capture the exact read/write behavior in the ADR appendix.
```bash
npx expo prebuild --clean
npx expo run:ios # or run:android
```

## Rollback plan

If `react-native-nfc-manager` is incompatible with the Expo dev-client:

1. Re-evaluate with a custom `expo prebuild` workflow and explicit native module linking.
2. If the library cannot be used, isolate NFC support behind a modular adapter and retain the ability to switch to a different NFC package or a pure native module.
1. Re-evaluate with explicit native module linking via `expo prebuild`
2. Isolate NFC behind `NfcService` adapter to swap library without UI changes

## Known limitations
## References

- Payload size: JSON roundtrip payloads must be kept small to avoid tag write/read failures.
- Platform differences: iOS and Android may behave differently, so the ADR must capture exact device compatibility notes.
- Native build required: NFC validation is only reliable on an Expo dev-client or prebuilt binary.
- iOS Core NFC only supports certain tag types and cannot run on simulator hardware.
- [react-native-nfc-manager Expo wiki](https://github.com/revtel/react-native-nfc-manager/wiki/Expo-Go)
- Product spec: `docs/ding-payments.md` — Proposed Payment Payload Structure
4 changes: 4 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
process.env.EXPO_PUBLIC_STELLAR_NETWORK = 'testnet';
process.env.EXPO_PUBLIC_HORIZON_URL = 'https://horizon-testnet.stellar.org';
process.env.EXPO_PUBLIC_RPC_URL = 'https://soroban-testnet.stellar.org';
process.env.EXPO_PUBLIC_USDC_ISSUER = 'GBBD47IF6LWK7P7MUGHC2XLYUUXV6ZLW75PN7CHLIW2NSIW74UZEST66';
Loading
Loading