Skip to content

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

PieceState
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 functionsScaffolded (this repo) — deploy + validate against a PortOne TEST channel
@portone/react-native-sdk native adapter + registry swapPending 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

  1. Create/confirm your Store → note the Store ID (store-...).
  2. Add a Channel (test first: a test PG like KG이니시스/토스페이먼츠 test) → note the Channel Key (channel-key-...).
  3. 결제연동 → V2 API secret → copy the API Secret.
  4. 웹훅(Webhook): set the Endpoint URL to the deployed portone-webhook function (https://<project-ref>.supabase.co/functions/v1/portone-webhook) → copy the Webhook Secret (whsec_...). Enable at least Transaction.Paid (+ Transaction.VirtualAccountIssued for 가상계좌).

2. Supabase secrets + deploy the edge functions

bash
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 signature

Both 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.

bash
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 on onComplete / rejects on onError.
  • Generate a unique paymentId per attempt (e.g. sp-${sessionId}-${userId}-${ts}).
  • For 가상계좌, set payMethod:'VIRTUAL_ACCOUNT' + a validHours equal to the hold window (10) — PortOne then fires the Transaction.Paid webhook 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-payment returns {paid:true} for a TEST card payment; the hold flips to paid (참가 확정).
  • [ ] amount tampering (client sends a wrong amount) is rejected by the RPC amount_mismatch.
  • [ ] 가상계좌: issue → the portone-webhook marks paid on the TEST deposit.
  • [ ] webhook signature failure → 401; stale timestamp → 400.
  • [ ] manual rail still works (송금 완료 → 확인 대기 → host confirm).

Markdown remains the source of truth. Run yarn docs:check before handoff.