@paysio/react-native
v0.1.1
Published
Paysio Mobile SDK for React Native and Expo: native card and bank entry, 3-D Secure, and checkout.
Maintainers
Readme
@paysio/react-native
Paysio Mobile SDK for React Native and Expo. Native card and bank account entry, 3-D Secure, and a drop-in checkout sheet. Card numbers go straight from the secure fields to the Paysio vault (VGS). They never reach your app code, your server, or Paysio's servers.
Works in Expo Go and in bare React Native. No native code to link. The only extra packages are react-native-webview (for 3-D Secure) and react-native-svg (for the card brand marks and icons).
Install
npm i @paysio/react-native react-native-webview react-native-svgWith Expo:
npx expo install react-native-webview react-native-svg
npm i @paysio/react-nativeRequirements: React Native 0.71 or newer, React 18 or newer, react-native-svg 13 or newer, react-native-webview 11 or newer.
Quickstart
On your server, create a checkout session with your secret key. The response contains a
slug. Hand that slug to the app (never ship a secret key in the app).curl https://api.paysio.com/v1/checkout-sessions \ -H "Authorization: Bearer sk_live_..." \ -H "Content-Type: application/json" \ -d '{ "items": [{ "productId": "prod_...", "quantity": 1 }], "customerEmail": "[email protected]" }'In the app, render the payment sheet with that slug.
import { PaysioProvider, PaysioPaymentSheet } from '@paysio/react-native' export function CheckoutScreen({ slug }: { slug: string }) { return ( <PaysioProvider sessionSlug={slug} theme={{ scheme: 'auto' }} appId="com.example.app"> <PaysioPaymentSheet onSuccess={(result) => console.log('paid', result.transactionId, result.orderLabel)} onError={(error) => console.log(error.code, error.message)} /> </PaysioProvider> ) }Handle
onSuccess. The result carriestransactionId,orderLabel,status(pending_settlement),amount,currencyandcard: { brand, last4 }. Confirm the order on your server from the Paysio webhook, not from the app.Test with sandbox cards. Create the session in a sandbox workspace and use these numbers with any future expiry and any CVC:
| Card | Number | |---|---| | Visa, no 3-D Secure | 4111 1111 1111 1111 | | Visa, 3-D Secure frictionless | 4147 4630 1111 0134 | | Visa, 3-D Secure challenge | 4016 3600 0000 0493 |
Bank account (sandbox): routing
490000018, account24413815. Other routing numbers are refused in the sandbox.Go live. Create sessions with your live secret key. Nothing changes in the app.
Custom layout
The sheet is built from pieces you can arrange yourself. Everything reads from usePaysio().
import { Button, View } from 'react-native'
import {
PaysioProvider, usePaysio,
PaysioContactFields, PaysioCardFields, PaysioBillingAddress,
} from '@paysio/react-native'
function Form() {
const { session, state, pay, customer, setCustomer, billing, setBilling } = usePaysio()
if (!session) return null
return (
<View style={{ gap: 24 }}>
<PaysioContactFields value={customer} onChange={setCustomer} requirePhone={session.requirePhone} hideName />
<PaysioCardFields />
<PaysioBillingAddress value={billing} onChange={setBilling} errors={state.fieldErrors} />
<Button title={`Pay ${session.totalFormatted}`} disabled={!state.canPay} onPress={() => pay()} />
</View>
)
}
export function Screen({ slug }: { slug: string }) {
return (
<PaysioProvider sessionSlug={slug}>
<Form />
</PaysioProvider>
)
}pay(options?) validates, tokenizes, runs 3-D Secure when the merchant has it on, and charges. It returns the same result as the sheet and throws a PaysioError on failure. state has canPay, isPaying, step (idle, validating, aliasing, three_ds, charging, done), fields.number / expiry / cvc, brand, last4 and fieldErrors.
Other pieces: PaymentMethodPicker (the radio rows with collapsible panels), PaysioBankFields, PaysioCustomFields, SavedMethodSelect, AcceptedBrandStack, CardBrand, Icon, StackedInput, BottomSheet.
3-D Secure
When the merchant has 3-D Secure on, pay() runs it before the charge. Most cards pass silently. When the bank asks the cardholder to confirm, the SDK opens a white sheet titled "Verify your card" with the bank's own page inside. The sheet closes by itself when the bank answers. If the customer taps Cancel, pay() throws three_ds_cancelled and nothing is charged. If the bank does not answer within 4 minutes, pay() throws three_ds_timeout.
The sheet is part of PaysioProvider, so it works in custom layouts too. Nothing to mount.
Bank accounts
When the merchant accepts bank payments, the sheet shows a "Bank account" row with a routing number field, an account number field, Personal / Business and Checking / Savings toggles, and the authorization text the customer must accept. The account number goes to the vault the same way a card number does. Bank payments settle later; the result status is pending_settlement and the final outcome arrives by webhook.
Address suggestions
The street line in the billing and shipping address suggests real addresses as the customer types, the same as hosted checkout. Picking one fills the street, city, state, ZIP and country. Suggestions come from Google Places through Paysio's API, so there is no key to add to your app. Inside PaysioProvider this is on automatically. If suggestions are unavailable, the field works as normal text.
Saved methods
Saved methods are a phase 2 feature. PaysioProvider accepts savedMethods and savedMethodPortalToken; when both are given the sheet renders a "Saved method" row. Obtaining the portal token (email code) is not part of this release.
Theming
Pass theme to PaysioProvider:
<PaysioProvider
sessionSlug={slug}
theme={{
scheme: 'auto', // 'light' | 'dark' | 'auto'
colors: { primary: '#0B57D0' },
darkColors: { card: '#111111' },
fontFamily: 'Inter',
fontFamilyMedium: 'Inter-Medium',
radius: 18, // outer radius of the picker
}}
>| Token | Light | Dark | Used for |
|---|---|---|---|
| primary | #0B57D0 | #0B57D0 | Pay button, selected radio, links |
| foreground | #1B1B1B | #F0F0F0 | Text, input text |
| mutedForeground | #5D5D5F | #A6A6A6 | Headings, placeholders, icons |
| card | #FCFCFD | #101010 | Sheets, saved method trigger |
| background | #F3F3F4 | #0A0A0A | Chip press state |
| secondary | #EAEAED | #181818 | Saved address trigger, chips |
| border | #ECECEC | rgba(255,255,255,0.09) | Picker border and dividers |
| input | #DBDBDC | #404040 | Field borders |
| rowHover | rgba(0,0,0,0.04) | rgba(255,255,255,0.05) | Row press |
| ring | rgba(0,0,0,0.35) | rgba(255,255,255,0.4) | Focus ring, same as hosted checkout |
| destructive | #C70A24 | #DC2626 | Errors |
The focus ring stays grey when you change colors.primary, the same as the hosted checkout. Set ring yourself if you want a coloured one. The 3-D Secure sheet is always white, because the bank's page inside it is.
Errors
pay() throws PaysioError. code is stable; message is plain language you can show.
| Code | Meaning | What to do |
|---|---|---|
| invalid_fields | Something is missing or invalid. fieldErrors has a message per field. | Show the messages |
| billing_address_required | Street, city, postal code or country is missing | Show the billing block |
| contact_required | Email, first name or last name is missing (or phone when required) | Show the contact block |
| declined | The bank declined. declineCode has the processor code. | Ask for another card |
| three_ds_failed | Card verification was refused | Ask for another card |
| three_ds_cancelled | The customer closed the verification sheet | Let them try again |
| three_ds_timeout | The bank did not answer in 4 minutes | Let them try again |
| session_expired | The session is expired, inactive or already paid | Create a new session |
| session_invalid | The slug is empty or not a checkout | Check the slug |
| rate_limited | Too many attempts | Wait, then retry |
| network | Paysio could not be reached before anything was charged | Retry |
| unknown | The charge was sent but the answer did not arrive. idempotencyKey is set. | Do not retry blindly. Confirm by webhook or ask support with the key. |
| vault_unavailable | The secure fields could not be submitted | Retry |
| not_configured | The session has no vault configuration | Contact Paysio |
unknown deserves care: the SDK already retried the charge once with the same idempotency key when the network dropped. A second attempt with the same key is safe (Paysio replays the stored answer), a new attempt is not.
Events
onEvent on PaysioProvider receives analytics events. None of them contains card data.
ready, field_focus, method_change, pay_start, three_ds_start, three_ds_challenge, three_ds_result, pay_result, error.
App Store and Google Play rules
Physical goods and real-world services (clothes, food delivery, a plumber, a gym booking) must not use in-app purchase. Collecting a card in the app is allowed and carries no store fee. That is what this SDK is for.
Digital goods and services consumed inside the app (subscriptions to app features, coins, unlocks) must use the store's own billing on both platforms. Do not use this SDK for those sales. See Apple App Store Review Guideline 3.1.3(e) and the Google Play Payments policy.
Security and PCI
Card data is typed into fields provided by VGS and sent directly to VGS's PCI DSS Level 1 environment. It is not transmitted to or stored by your servers or by Paysio's servers; Paysio only ever receives an alias. Your app is still the point of entry, so:
- Keep the SDK up to date.
- Do not take screenshots or screen recordings of the payment screen in your own code (on Android consider
FLAG_SECUREfor that screen). - Never put a secret key (
sk_) or a publishable key (pk_) in the app. The session slug is the only thing the app needs. onEventandPaysioErrornever carry card data; it is safe to log them.
Expo notes
- Works in Expo Go. No config plugin, no development build needed.
- iOS privacy manifest: the VGS SDK declares its API usage in its own
PrivacyInfo.xcprivacy. If your app ships its own manifest, copy the entries fromnode_modules/@vgs/collect-react-native/PrivacyInfo.xcprivacy(when present in your installed version) into it. They are "Product Interaction" entries, not tracking. - Android: nothing to add.
Using the fields without a checkout session (advanced)
Paysio's own app and some integrations need the secure fields without a session. PaysioFieldsProvider gives the fields a vault and a theme; usePaysioFields() returns aliasCard() and aliasBank(), which submit the mounted fields and return aliases you post to your own endpoint.
import { Button } from 'react-native'
import { PaysioFieldsProvider, PaysioCardFields, usePaysioFields } from '@paysio/react-native'
function AddCard({ onAlias }: { onAlias: (a: { panAlias: string; cvcAlias: string; expMonth: string; expYear: string }) => void }) {
const { aliasCard, cardState } = usePaysioFields()
return (
<>
<PaysioCardFields />
<Button title="Save card" disabled={!cardState.valid} onPress={async () => onAlias(await aliasCard())} />
</>
)
}
export function Screen() {
return (
<PaysioFieldsProvider vault={{ vaultId: 'tnt...', environment: 'sandbox' }} sandbox theme={{ scheme: 'dark' }}>
<AddCard onAlias={(a) => fetch('https://your.server/cards', { method: 'POST', body: JSON.stringify(a) })} />
</PaysioFieldsProvider>
)
}Pass apiBase="https://api.paysio.com" to PaysioFieldsProvider (or to PaysioBillingAddress directly) to turn on address suggestions outside a session. createCollector({ vaultId, environment }), aliasCard(collector, meta) and aliasBank(collector, meta) are exported for code that manages the VGS collector itself. PaymentMethodPicker, PaysioBillingAddress, SavedMethodSelect and the other pieces are plain controlled components and work inside either provider.
Keeping fields clear of the keyboard
When a field gets focus, or the address suggestions open, the SDK scrolls the page just enough that they sit above the keyboard, sliding up together with it. PaysioPaymentSheet does this on its own.
In a custom layout, the SDK needs to scroll your scroll view. Either use PaysioScrollView in place of your ScrollView, or wrap your content in PaysioKeyboardScrollProvider and tell it how to scroll:
import { useRef } from 'react'
import { ScrollView } from 'react-native'
import { PaysioKeyboardScrollProvider, PaysioCardFields } from '@paysio/react-native'
export function Checkout() {
const ref = useRef<ScrollView>(null)
const offset = useRef(0)
return (
<ScrollView ref={ref} onScroll={(e) => (offset.current = e.nativeEvent.contentOffset.y)} scrollEventThrottle={16}>
<PaysioKeyboardScrollProvider scrollBy={(dy) => ref.current?.scrollTo({ y: offset.current + dy, animated: true })}>
<PaysioCardFields />
</PaysioKeyboardScrollProvider>
</ScrollView>
)
}Without either, the fields still work; they just do not scroll themselves into view.
Using your own sheets and list rows
The country, state, saved address, saved card and custom select lists open in a bottom sheet. If your app already has its own sheet and list rows, pass them in and every list matches the rest of your app:
import { PaysioSheetComponentsProvider, type PaysioSheetComponents } from '@paysio/react-native'
const sheets: PaysioSheetComponents = {
Sheet: MySheet, // { open, onClose, title, searchable, searchPlaceholder, children: (query) => ReactNode }
Row: MyRow, // { selected, onPress, leading, label, detail, trailing, disabled, children }
Empty: MyEmpty, // { text }
}
<PaysioSheetComponentsProvider components={sheets}>{/* Paysio fields */}</PaysioSheetComponentsProvider>Each part is optional. Anything you leave out uses the SDK's own look.
Changelog
0.1.1
Street address suggestions (Google Places through Paysio's API) in PaysioBillingAddress. New apiBase prop on PaysioFieldsProvider and PaysioBillingAddress. Fields and the suggestion list now stay clear of the keyboard: the drop-in sheet handles it, and custom layouts use PaysioKeyboardScrollProvider or PaysioScrollView. Pick-list sheets sit above the keyboard while you search, and can be replaced with your own through PaysioSheetComponentsProvider. Fixed a "Cannot update a component while rendering a different component" warning when onEvent sets state. The focus ring now matches hosted checkout (grey, not blue) and the CVC is no longer masked, same as the web.
0.1.0
First release. Drop-in PaysioPaymentSheet, composable fields, 3-D Secure sheet, bank accounts, light and dark themes, Expo Go support.
