PortOne v2 payments — setup guide
Status: Reference
In-app payment for session participation fees (and, later, dues), via PortOne v2 WebView (rule KR-3 — never per-PG SDKs directly). Runs alongside manual 계좌이체 tracking — both rails write the same session_payments hold.
Where it stands
| Piece | State |
|---|---|
| Hold model + 10-min from-reservation timer + manual 송금 완료 / 확인 대기 | Shipped (migrations 00298–00301; OTA) |
PaymentServicePort + mock adapter (debug, OTA-safe) | Shipped — 결제하기 works end-to-end for the dev account via dev_mock_pay_session |
mark_session_payment_paid_via_portone service RPC (00302) | Shipped + probe-verified (paid / idempotent / amount-mismatch) |
verify-portone-payment + portone-webhook edge functions | Scaffolded (this repo) — deploy + validate against a PortOne TEST channel |
@portone/react-native-sdk native adapter + registry swap | Pending the EAS build (native dep → new runtime) |
The whole gate is debuggable today with the mock (no PortOne, no native build). The steps below flip the real rail on.
1. PortOne console
- Create/confirm your Store → note the Store ID (
store-...). - Add a Channel (test first: a test PG like KG이니시스/토스페이먼츠 test) → note the Channel Key (
channel-key-...). - 결제연동 → V2 API secret → copy the API Secret.
- 웹훅(Webhook): set the Endpoint URL to the deployed
portone-webhookfunction (https://<project-ref>.supabase.co/functions/v1/portone-webhook) → copy the Webhook Secret (whsec_...). Enable at leastTransaction.Paid(+Transaction.VirtualAccountIssuedfor 가상계좌).
2. Supabase secrets + deploy the edge functions
supabase secrets set PORTONE_V2_API_SECRET=<v2 api secret>
supabase secrets set PORTONE_WEBHOOK_SECRET=whsec_<...>
# TWOMORE_SECRET_KEY (service-role) is already set.
supabase functions deploy verify-portone-payment # client calls with the user JWT
supabase functions deploy portone-webhook --no-verify-jwt # PortOne calls; auth is the signatureBoth return 503 not_configured until the secrets are present — safe to deploy early.
3. Native adapter + EAS build (the last mile)
The RN SDK is a native dependency → a new EAS build (NOT OTA-able). Do this on a build commit, then bump app.json version so the new runtime is fingerprinted.
cd apps/mobile
npx expo install @portone/react-native-sdk react-native-webview
npm i -D @portone/browser-sdk
# app.json → "plugins": [..., "@portone/react-native-sdk/plugin"]Then add a real adapter mirroring packages/app/src/adapters/payment/mock-payment.service.ts:
createPortonePaymentService()opens the SDK<Payment request={{ storeId, channelKey, paymentId, orderName, totalAmount, currency:'CURRENCY_KRW', payMethod, customer, customData: JSON.stringify({ sessionId, userId }) }} />, resolves ononComplete/ rejects ononError.- Generate a unique
paymentIdper attempt (e.g.sp-${sessionId}-${userId}-${ts}). - For 가상계좌, set
payMethod:'VIRTUAL_ACCOUNT'+ avalidHoursequal to the hold window (10) — PortOne then fires theTransaction.Paidwebhook on deposit.
IMPORTANT (OTA safety): do NOT top-level-import @portone/react-native-sdk from a module that ships in an OTA bundle before the native build lands — a top-level requireNativeModule throws outside userland try/catch and crashes cold start (see the memory note). Keep the registry on the mock until the build ships, then swap registry.ts's payment: to the PortOne adapter behind a native guard.
4. Wire the client checkout to the real verify
Today useSessionPaymentCheckout calls sessionPayments.devMockConfirm after a mock "success". For the real rail, after the PortOne adapter resolves, call the verify-portone-payment function instead (server confirms with PortOne, then the 00302 RPC marks paid). Swap that one call; the hold/UI/state machine is unchanged.
5. The money flow + the 3% fee
Per the revenue model, participation fees route through PortOne with a 3% platform fee. Settlement to clubs (sub-merchant / marketplace) is a separate design — out of scope here; v1 can collect to one store and reconcile, or gate in-app pay to clubs once club-level settlement lands.
Testing checklist (PortOne TEST channel)
- [ ]
verify-portone-paymentreturns{paid:true}for a TEST card payment; the hold flips topaid(참가 확정). - [ ] amount tampering (client sends a wrong amount) is rejected by the RPC
amount_mismatch. - [ ] 가상계좌: issue → the
portone-webhookmarks paid on the TEST deposit. - [ ] webhook signature failure → 401; stale timestamp → 400.
- [ ] manual rail still works (송금 완료 → 확인 대기 → host confirm).