Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
47f78ff
feat(merchant-pos-app): add Expo merchant POS (self-onboarding + WCPay)
ganchoradkov May 27, 2026
ff67978
feat(merchant-pos-app): payment links with real 10d expiry + native s…
ganchoradkov May 28, 2026
047a646
feat(merchant-pos-app): self-onboarded merchant via pay-core + UX polish
ganchoradkov May 28, 2026
a869f17
feat(merchant-pos-app): self-onboarded merchant via pay-core + UX polish
ganchoradkov May 28, 2026
a460c22
refactor(merchant-pos-app): source partnerId in the merchant service
ganchoradkov May 28, 2026
78900a2
fix(merchant-pos-app): count paid links in activity; sign once per se…
ganchoradkov May 29, 2026
06132f6
fix(merchant-pos-app): clear signing progress on disconnect
ganchoradkov Jun 1, 2026
2afc0fb
ci(merchant-pos-app): add Create iOS App + Release Merchant POS workf…
ignaciosantise Jun 1, 2026
3f45125
fix(merchant-pos-app): produce reads ASC API token globally, not via …
ignaciosantise Jun 1, 2026
a261f91
fix(merchant-pos-app): pass APPLE_USERNAME to produce
ignaciosantise Jun 1, 2026
c3313b9
fix(merchant-pos-app): drop produce; app record is created manually
ignaciosantise Jun 1, 2026
2b6dc16
docs(merchant-pos-app): correct Create iOS App prerequisite comment
ignaciosantise Jun 1, 2026
3e2944c
ci(merchant-pos-app): set real Apple App ID for iOS release
ignaciosantise Jun 2, 2026
36182f9
fix(merchant-pos-app): branch certs from master over SSH in --no-pr mode
ignaciosantise Jun 2, 2026
b741473
fix(merchant-pos-app): set ios.appleTeamId for code signing
ignaciosantise Jun 2, 2026
ffd994f
ci(merchant-pos-app): rename workflow to 'Create iOS Certificates'
ignaciosantise Jun 2, 2026
652aecf
ci(merchant-pos-app): rename workflow file to create-ios-certs.yaml
ignaciosantise Jun 2, 2026
9ce2d3e
docs(merchant-pos-app): add new-app release runbook; point README at …
ignaciosantise Jun 2, 2026
ac5be43
docs(merchant-pos-app): document TestFlight distribution + public links
ignaciosantise Jun 2, 2026
698fa19
refactor(merchant-pos-app): create merchants via public WCPay REST API
ganchoradkov Jun 5, 2026
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
79 changes: 79 additions & 0 deletions .github/workflows/create-ios-certs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Create iOS Certificates

run-name: "Create iOS Certificates - ${{ inputs.bundle-id }}"

# Creates the signing certificates / provisioning profiles for an iOS app on CI, so
# developers don't need Ruby / fastlane / CocoaPods locally. Auth uses the App Store
# Connect API key (no Apple ID / 2FA).
#
# PREREQUISITE: the app's bundle id / App ID must already be registered in the Apple
# Developer Portal, and the App Store Connect app record must already exist. `match` does
# NOT create the identifier — it fails with "Couldn't find bundle identifier" otherwise.
# Both are created manually (App Store Connect → Apps → +) because fastlane `produce` only
# supports Apple ID + 2FA auth. See docs/releasing-a-new-app.md.

permissions:
contents: read

on:
workflow_dispatch:
inputs:
bundle-id:
description: "App bundle identifier (e.g. com.reown.merchantpos)"
required: true
type: string
certs-repo:
description: "Match certificates repo (owner/repo)"
required: true
default: 'reown-com/mobile-match'
type: string
match-types:
description: "Comma-separated match types to create"
required: true
default: 'appstore,development'
type: string

jobs:
create-certs:
runs-on: macos-latest-xlarge
steps:
- name: Checkout
uses: actions/checkout@v4

- name: webfactory/ssh-agent
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.MATCH_SSH_KEY }}

- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true

# Certs are pushed to a branch over SSH (MATCH_SSH_KEY); no GitHub token is used.
# A teammate opens & merges the PR in the certs repo (see job summary).
- name: Create certificates
env:
APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
MATCH_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
run: |
chmod +x scripts/create-certificates.sh
IFS=',' read -ra TYPES <<< "${{ inputs.match-types }}"
{
echo "### ⚠️ Action required: merge the certificates PR(s)"
echo "Certs were pushed to branch(es) in \`${{ inputs.certs-repo }}\`. The release"
echo "workflow will fail until they are merged into \`master\`:"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
for type in "${TYPES[@]}"; do
type="$(echo "$type" | xargs)" # trim whitespace
echo "::group::Creating ${type} certificates"
./scripts/create-certificates.sh "${{ inputs.certs-repo }}" "${{ inputs.bundle-id }}" "" "$type" --no-pr
echo "::endgroup::"
branch="certs/add-${{ inputs.bundle-id }}-${type}"
compare="https://github.com/${{ inputs.certs-repo }}/compare/master...${branch}?expand=1"
echo "- **${type}**: [open PR for \`${branch}\`](${compare})" >> "$GITHUB_STEP_SUMMARY"
done
70 changes: 70 additions & 0 deletions .github/workflows/release-merchant-pos.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Release Merchant POS
run-name: "Merchant POS - ${{ inputs.platform == 'both' && '🍎 iOS & 🤖 Android' || inputs.platform == 'ios' && '🍎 iOS' || '🤖 Android' }}"

permissions:
id-token: write
contents: read

on:
workflow_dispatch:
inputs:
platform:
description: 'Platform to build'
required: true
type: choice
options:
- ios
- android
- both

jobs:
release-android:
if: ${{ inputs.platform == 'android' || inputs.platform == 'both' }}
uses: ./.github/workflows/release-android-base.yaml
with:
name: 'Merchant POS React Native'
root-path: 'dapps/merchant-pos-app'
release-type: 'production'
project-type: 'dapp'
output-path: 'dapps/merchant-pos-app/android/app/build/outputs/apk/release/app-release.apk'
package-manager: 'npm'
is-expo-project: true
firebase-app-id: ${{ vars.MERCHANTPOS_ANDROID_FIREBASE_APP_ID }}
secrets:
env-file: ${{ secrets.MERCHANTPOS_ENV_FILE }}
sentry-file: ${{ secrets.MERCHANTPOS_SENTRY_FILE }}
secrets-file: ${{ secrets.ANDROID_SECRETS_FILE }}
gsa-key: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_KEY }}
keystore-name: ${{ vars.WC_PROD_KEYSTORE_NAME }}
keystore: ${{ secrets.WC_PROD_KEYSTORE }}
aws-account-id: ${{ secrets.AWS_ACCOUNT_ID }}
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
firebase-url: ${{ vars.FIREBASE_MERCHANTPOS_URL }}

release-ios:
if: ${{ inputs.platform == 'ios' || inputs.platform == 'both' }}
uses: ./.github/workflows/release-ios-base.yaml
with:
name: 'Merchant POS React Native'
root-path: 'dapps/merchant-pos-app'
release-type: 'production'
scheme-name: 'MerchantPOS'
bundle-id: 'com.reown.merchantpos'
apple-id: '6775838853'
project-type: 'dapp'
package-manager: 'npm'
testflight-groups: 'Internal'
is-expo-project: true
secrets:
env-file: ${{ secrets.MERCHANTPOS_ENV_FILE }}
sentry-file: ${{ secrets.MERCHANTPOS_SENTRY_FILE }}
apple-username: ${{ secrets.APPLE_USERNAME }}
apple-key-id: ${{ secrets.APPLE_KEY_ID }}
apple-key-content: ${{ secrets.APPLE_KEY_CONTENT }}
apple-issuer-id: ${{ secrets.APPLE_ISSUER_ID }}
match-username: ${{ secrets.MATCH_USERNAME }}
match-keychain-password: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
match-git-url: ${{ secrets.MATCH_GIT_URL }}
match-ssh-key: ${{ secrets.MATCH_SSH_KEY }}
slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
testflight-url: ${{ vars.TESTFLIGHT_MERCHANTPOS_URL }}
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,17 @@ bundle install

### Creating Certificates for a New App

Use the provided script to create new certificates and provisioning profiles. The script handles creating a branch, running fastlane match, and creating a PR (required since the certificates repo has branch protection):
**Preferred: do it on CI.** You don't need Ruby/fastlane/CocoaPods locally. Run the
**Create iOS Certificates** GitHub Action (`.github/workflows/create-ios-certs.yaml`) — it
creates the app's signing certificates via the App Store Connect API key (no 2FA). The App
Store Connect app record itself is created manually first; see the full runbook in
[`docs/releasing-a-new-app.md`](docs/releasing-a-new-app.md).

**Local fallback.** Use the provided script to create new certificates and provisioning
profiles. The script handles creating a branch, running fastlane match, and creating a PR
(required since the certificates repo has branch protection). When the App Store Connect API
key env vars (`APPLE_KEY_ID`, `APPLE_ISSUER_ID`, `APPLE_KEY_CONTENT`) are set, it uses API-key
auth (no 2FA); otherwise it falls back to interactive Apple ID auth:

```bash
# Make the script executable (first time only)
Expand All @@ -76,8 +86,11 @@ chmod +x scripts/create-certificates.sh
**Example:**

```bash
./scripts/create-certificates.sh reown-com/mobile-certificates com.reown.myapp dev@reown.com appstore
./scripts/create-certificates.sh reown-com/mobile-certificates com.reown.myapp dev@reown.com development
./scripts/create-certificates.sh reown-com/mobile-match com.reown.myapp dev@reown.com appstore
./scripts/create-certificates.sh reown-com/mobile-match com.reown.myapp dev@reown.com development

# With API-key auth (no 2FA), the Apple email is optional — pass "" instead:
./scripts/create-certificates.sh reown-com/mobile-match com.reown.myapp "" appstore --auto-merge
```

> **Note:** Requires [GitHub CLI](https://cli.github.com/) (`gh`) to be installed and authenticated. By default, the script creates a PR that requires manual merge. Use `--auto-merge` to automatically merge.
Expand Down
12 changes: 12 additions & 0 deletions dapps/merchant-pos-app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
EXPO_PUBLIC_PROJECT_ID=""

# WalletConnect Pay (WCPay) API — payment rail used by the POS + payment links,
# and the merchant + settlement management API (POST /v1/merchants,
# /v1/merchants/{id}/settlements/crypto).
# Include the /v1 path segment, e.g. https://api.pay.walletconnect.com/v1
#
# The partner-scoped customer API key authenticates every request (sent as the
# Api-Key header). The Merchant-Id is the server-assigned id (mrch_…) returned
# when the merchant is created at onboarding finish — not sourced from env.
EXPO_PUBLIC_API_URL=""
EXPO_PUBLIC_DEFAULT_CUSTOMER_API_KEY=""
33 changes: 33 additions & 0 deletions dapps/merchant-pos-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files

# dependencies
node_modules/

# Expo
.expo/
dist/
web-build/
expo-env.d.ts

# Native
/ios
/android

# Metro
.metro-health-check*

# debug
npm-debug.*
yarn-debug.*
yarn-error.*

# macOS
.DS_Store
*.pem

# local env files
.env*.local
.env

# typescript
*.tsbuildinfo
114 changes: 114 additions & 0 deletions dapps/merchant-pos-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Merchant POS

A standalone Expo React Native app where a merchant onboards themselves, connects their own
settlement wallet (EVM + Solana) via **Reown AppKit**, and runs crypto POS payments and shareable
payment links through the **WalletConnect Pay (WCPay)** API.

In V1 the **connected wallet is the merchant identity** — onboarding is lightweight and stored
locally on the device.

## Screens

| Flow | Screen | Route |
| ---------- | --------------------------------------------------- | ---------------------------------------------- |
| Welcome | Value prop, Get started / Log in | `app/index.tsx` |
| Onboarding | Business details (email, company, logo) | `app/onboarding/business-details.tsx` |
| | Settlement networks (Ethereum / Solana) | `app/onboarding/networks.tsx` |
| | Connect wallet (AppKit, "already registered" guard) | `app/onboarding/connect-wallet.tsx` |
| | Verify ownership (sign message) | `app/onboarding/verify.tsx` |
| | Choose tokens | `app/onboarding/tokens.tsx` |
| Home | Merchant card, stats, actions, recent activity | `app/home.tsx` |
| POS | Amount entry (numpad + currency) | `app/pos/amount.tsx` |
| | Checkout: QR + polling + 15-min expiry + cancel | `app/pos/checkout.tsx` |
| | Payment received / cancelled-expired-failed | `app/pos/success.tsx`, `app/pos/cancelled.tsx` |
| Links | List, create, native share (10-day validity) | `app/links/index.tsx` |
| Activity | Locally tracked payment history | `app/activity.tsx` |

## Architecture

- **Navigation:** Expo Router (file-based, `headerShown: false` with custom in-screen headers).
- **Wallet:** Reown AppKit (`@reown/appkit-react-native` + wagmi & Solana adapters) initialized in
`app/_layout.tsx`. AppKit sessions persist via an AsyncStorage adapter (`utils/appkit-storage.ts`).
- **State:** Zustand stores persisted to MMKV (`utils/storage.ts`):
- `useMerchantStore` — registry of onboarded wallets keyed by address + the active session.
- `useOnboardingStore` — in-memory draft committed on "Finish setup".
- `useSettingsStore` — theme + currency.
- `usePaymentsStore` / `usePaymentLinksStore` — locally tracked payments and links.
- **Payments:** WCPay REST via `services/` (`startPayment` / `getPaymentStatus` / `cancelPayment`),
with React Query polling in `services/hooks.ts`.
- **Theme:** light + dark token system (`constants/theme.ts`, `hooks/use-theme-color.ts`); defaults
to dark, matching the prototype.

### Identity ↔ payments

A persistent **install id** (`utils/install-id.ts`, MMKV — survives launches, wiped on uninstall)
is the merchant id. On onboarding finish the app calls **PUT
`{EXPO_PUBLIC_PAY_CORE_API_URL}/v2/internal/merchant`** with a Cognito access token (minted via
client_credentials, cached for 50 min — see `services/cognito-auth.ts`), passing the install id,
business name, settlement networks (CAIP-10 MTAs + CAIP-19 tokens), and `partnerId`. The local
`MerchantConfig` records the `merchantId` and `version`; re-onboarding bumps the version.

WCPay payment calls (`startPayment` / status / cancel) then go to the customer API with
`Merchant-Id` = the active merchant's id (the one we just created), `Api-Key` = the partner-scoped
key from env. The returned `gatewayUrl` is what the customer scans/opens.

> AppKit's network set is fixed at `createAppKit` time, so the Screen-3 network selection is stored
> as a settlement preference and rendered as scope rather than re-scoping AppKit at runtime.
Comment on lines +55 to +56

## Setup

```bash
npm install
cp .env.example .env # fill in values
```

`.env`:

```bash
EXPO_PUBLIC_PROJECT_ID="" # Reown AppKit project id — https://dashboard.reown.com
EXPO_PUBLIC_API_URL="" # WCPay API base URL (include /v1)
EXPO_PUBLIC_DEFAULT_CUSTOMER_API_KEY="" # WCPay api key (partner-scoped)

# Merchant-Id is no longer env-sourced. It's the install-bound id of the
# merchant we created at onboarding finish via the pay-core upsert.

# Pay-core internal API (used to upsert the merchant on onboarding finish).
# Mirrors dashboard-new/src/server/clients/pay-core (PUT /v2/internal/merchant).
EXPO_PUBLIC_PAY_CORE_API_URL=""
EXPO_PUBLIC_PAY_PARTNER_ID=""

# Pay-core Cognito (OAuth2 client_credentials). The app mints an access token
# via these creds and caches it for 50 min, refreshing on 401.
EXPO_PUBLIC_PAY_CORE_COGNITO_TOKEN_ENDPOINT=""
EXPO_PUBLIC_PAY_CORE_COGNITO_CLIENT_ID=""
EXPO_PUBLIC_PAY_CORE_COGNITO_CLIENT_SECRET=""
EXPO_PUBLIC_PAY_CORE_COGNITO_SCOPE=""
```

## Merchant identity & upsert

A persistent install id (`utils/install-id.ts`, stored in MMKV — survives app
launches, wiped on uninstall) is minted on first launch and used as the
**merchant id**. When the user completes onboarding (Finish setup on the tokens
screen), the app builds a `MerchantUpsertRequest` from the draft + connected
addresses (per-namespace CAIP-10 MTAs + CAIP-19 tokens for the selected
networks) and PUTs it to `EXPO_PUBLIC_PAY_CORE_API_URL/v2/internal/merchant`
with the bearer token. The local merchant config records the `merchantId` and
`version`; re-onboarding bumps the version.

## Run

AppKit and several libraries ship native modules, so a development build is required:

```bash
npx expo prebuild # generate ios/ and android/
npm run ios # or: npm run android
```

## Checks

```bash
npx tsc --noEmit
npm run lint
npx prettier --write .
```
Loading