Coinme Vault SDK for React Native

Coinme Vault SDK for React Native

@coinme-security/vault-sdk-react-native is a typed JavaScript wrapper that lets your React
Native app capture debit card data — card number, expiry, CVC, cardholder name, and billing
address — without any of those values ever touching your JavaScript, your component state, your
logs, or your backend. You collect the data through secure fields provided by the SDK; it
travels directly to Coinme's PCI-compliant vault; your app receives only a confirmation
response.


How it works

The SDK is a thin wrapper around VGS Collect for React Native
(@vgs/collect-react-native). VGS
("Very Good Security") is a data-security platform that acts as a secure proxy between your
app and sensitive payment data. The SDK pre-binds Coinme's VGS vault configuration for ease of use, and it exposes convenience methods for high profile operations like adding payment cards to a customer account.

From your app's perspective, the flow is:

  1. You wrap your card form in a CoinmeVaultProvider.
  2. You render secure field components (CoinmeCardNumberField, CoinmeExpiryField,
    CoinmeCvcField, and others) that look and behave like ordinary React Native text inputs.
  3. Internally, each field holds its value inside the VGS collector — your application code
    has no access to the raw PAN, CVC, or expiry, even through refs.
  4. When the user submits, you call vault.submit(). The SDK routes the payload through VGS's
    secure proxy to Coinme's ingress, and returns a response object (status and parsed body).
    No raw card value ever passes through your code.

Pure JS — no native build step

Both this SDK and its @vgs/collect-react-native peer are pure JavaScript/TypeScript. They
use React Native's built-in TextInput under the hood and submit over fetch — there is no
native iOS/Android module. This means:

  • Expo Go works out of the box. No prebuild, no config plugin, no pod install or Gradle
    edits are needed for either package.
  • Neither React Native architecture (Bridge or new JSI/Fabric) requires any extra
    configuration.
  • The only native toolchain work required to build your app is whatever your own code already
    needs — the SDK adds nothing on top.
📘

Note:

If a future VGS release ships native code, an Expo development build would be requiredinstead of Expo Go. The SDK README will note this if it ever applies.


Prerequisites

Before you install the SDK, confirm you have the following:

RequirementMinimum
Node20+ (see .nvmrc in the SDK repo for the tested version)
React18 or 19
React Native0.76 or later
CLOUDSMITH_TEAM_TOKENCoinme Cloudsmith entitlement token — see below

For Expo projects, nothing beyond the above is needed. The app runs in Expo Go.

For bare React Native projects, you need the standard native toolchain for your target
platform (Xcode and CocoaPods for iOS; JDK 17 and the Android SDK for Android). The SDK adds
nothing to this — it is the same toolchain your app already requires.

Obtaining the CLOUDSMITH_TEAM_TOKEN

The SDK is published to Coinme's private npm registry. Access is authenticated by a
Cloudsmith entitlement token. This token is used only at npm install time — it does not
apply to pod install, Gradle, or any runtime requests. To obtain one, contact the Coinme
business development team.


Installation

The SDK is scoped under @coinme-security, which is hosted on Coinme's private registry. You
configure that scope in your project's package manager config, then install both the SDK and
its required peer dependency.

Step 1 — Configure the private registry

.npmrc (npm or Yarn Classic):

@coinme-security:registry=https://npm.coinme.com/coinme-security-wrapper/
//npm.coinme.com/coinme-security-wrapper/:_authToken=${CLOUDSMITH_TEAM_TOKEN}
always-auth=true

.yarnrc.yml (Yarn Berry / v2+):

nodeLinker: node-modules

npmScopes:
  coinme-security:
    npmRegistryServer: 'https://npm.coinme.com/coinme-security-wrapper/'
    npmAuthToken: '${CLOUDSMITH_TEAM_TOKEN}'
    npmAlwaysAuth: true

Commit the config file to your repo. The CLOUDSMITH_TEAM_TOKEN value itself should be
supplied as an environment variable and never committed to source control.

Step 2 — Install the packages

export CLOUDSMITH_TEAM_TOKEN=<your-token>
yarn add @coinme-security/vault-sdk-react-native @vgs/collect-react-native

@coinme-security/vault-sdk-react-native resolves from Coinme's private registry. Its peer
@vgs/collect-react-native resolves from the public npm registry. Both must be installed —
the SDK will not work without the peer.


Running your app

Because there is no native code in either package, there is no extra step to integrate them
into your native build.

Expo

Run in Expo Go — no prebuild required:

npx expo start
# then press `i` for iOS Simulator, `a` for Android emulator,
# or scan the QR code with Expo Go on a physical device

If your own app already requires native modules that need a development build, continue to use
your development build workflow — the SDK will work the same way there. You only need a
development build (npx expo prebuild) for reasons unrelated to this SDK.

Bare React Native

Run the standard native build for your platform:

# iOS
cd ios && pod install && cd ..
npx react-native run-ios

# Android
npx react-native run-android

The pod install step here installs React Native's own pods — not VGS or this SDK. Neither
package adds anything to your Podfile or Gradle configuration.

iOS privacy manifest (optional)

VGS provides an optional privacy manifest for @vgs/collect-react-native as a transparency
measure. It declares product-interaction analytics only — no personally identifiable information
and no user tracking are involved. Apple does not currently require this manifest for this SDK.
If your release process merges third-party privacy manifests into your app's
PrivacyInfo.xcprivacy, see
VGS privacy details
for the entries to add.


Quickstart

Here is the minimum integration — a card number, expiry, and CVC field, gated submit, and
error handling. If all you need is to capture a basic card form and submit it, this is it.

import {
  CoinmeCardNumberField,
  CoinmeExpiryField,
  CoinmeCvcField,
  CoinmeVaultError,
  CoinmeVaultProvider,
  useCoinmeVault,
  useVaultFormValidity,
  VAULT_FIELD_NAMES,
} from '@coinme-security/vault-sdk-react-native';
import { useState } from 'react';
import { Button, Text, View } from 'react-native';

const CardForm = () => {
  const vault = useCoinmeVault();
  const { isValid } = useVaultFormValidity();
  const [status, setStatus] = useState('');

  const onSubmit = async () => {
    try {
      const result = await vault.submit();
      // result.status is the HTTP status code; result.data is the parsed response body.
      // Raw card values are never available here.
      setStatus(`OK ${result.status}`);
    } catch (error) {
      if (error instanceof CoinmeVaultError) {
        setStatus(`${error.kind}: ${error.message}`);
      }
    }
  };

  return (
    <View>
      <CoinmeCardNumberField
        fieldName={VAULT_FIELD_NAMES.pan}
        placeholder="Card number"
      />
      <CoinmeExpiryField
        fieldName={VAULT_FIELD_NAMES.expiry}
        placeholder="MM/YY"
      />
      <CoinmeCvcField fieldName={VAULT_FIELD_NAMES.cvc} placeholder="CVC" />
      <Button title="Submit" onPress={onSubmit} disabled={!isValid} />
      <Text>{status}</Text>
    </View>
  );
};

// Wrap your form — or your entire screen — in CoinmeVaultProvider.
// Pass 'sandbox' while developing; switch to 'live' for production.
export const App = () => (
  <CoinmeVaultProvider environment="sandbox">
    <CardForm />
  </CoinmeVaultProvider>
);

The three things to notice:

  • CoinmeVaultProvider is the entry point. It creates one vault on mount and makes it
    available to everything inside it. Set environment to 'sandbox' for development and
    'live' for production — no other configuration is required to get started.
  • useVaultFormValidity() aggregates the validity of every registered field. Disabling the
    submit button until isValid is true prevents an invalid card from ever reaching the server.
  • vault.submit() routes the collected values through VGS to Coinme's ingress and returns
    a result object. On failure it throws a CoinmeVaultError — see
    Error handling below.

Core concepts

The provider

CoinmeVaultProvider creates and owns one VGS vault instance for its subtree. All field
components and hooks in the tree share that vault through React context. You configure it once
at the top of the tree — you do not pass vault credentials or ingress URLs to individual fields.

<CoinmeVaultProvider environment="sandbox">
  {/* your form */}
</CoinmeVaultProvider>

environment and the optional config prop are read once on mount. To change either,
unmount and remount the provider. The vault is disposed automatically on unmount.

PropTypeNotes
environment'sandbox' | 'live'Required. Selects the vault, ingress host, and submit path.
configRuntimeVaultConfig?Optional. Overrides the ingress host and/or submit path. Both sandbox and live entries must be provided; environment selects which is active.
onError(error: unknown) => voidOptional observe-only callback for vault initialization errors — use it for logging. Wrap the subtree in a React error boundary to recover from initialization failures.

Secure field components

Each Coinme*Field component wraps a VGS secure input and binds it to the provider's vault
collector. You style and use them like ordinary React Native text inputs. The raw value typed
by the user stays inside VGS — it is not exposed through the component's ref, onStateChange
callback, or any other prop.

ComponentPurpose
CoinmeCardNumberFieldCard number with Luhn validation, brand detection, and masking.
CoinmeExpiryFieldExpiry date in MM/YY format; validates that the date is in the future. On submit, splits into separate exp_month and exp_year values.
CoinmeCvcFieldSecurity code; adapts to the detected card brand (3 or 4 digits).
CoinmeCardHolderFieldCardholder name.
CoinmeTextFieldGeneric secure field for other sensitive values (billing address lines, ZIP code, SSN, etc.). Use the type prop to select behaviour.

Every field accepts all of the underlying VGS input's props — placeholder, onStateChange,
validationRules, maxLength, containerStyle, textStyle, and so on — except collector
(the SDK supplies this from the vault) and type (fixed on the dedicated card/CVC/expiry
fields).

Fields expose a ref with focus() and blur() for programmatic focus management.

Field names

Wire each field's fieldName prop to the corresponding constant from VAULT_FIELD_NAMES. This
keeps collected data aligned with the request body template used by submitPaymentMethod:

import { VAULT_FIELD_NAMES } from '@coinme-security/vault-sdk-react-native';

// Available names: pan, cvc, expiry, address1, address2, city, zipcode
<CoinmeCardNumberField fieldName={VAULT_FIELD_NAMES.pan} />

Hooks

useCoinmeVault() — returns the vault from the nearest CoinmeVaultProvider, with a
submit() method and the underlying VGS collector for advanced use. Throws if called outside
a provider.

useVaultFormValidity() — aggregates validity across every field registered with the
provider. isValid is true only when at least one field is registered and every registered
field is valid. fields is a map of all field states keyed by fieldName.

const { isValid, fields } = useVaultFormValidity();
<Button title="Submit" onPress={onSubmit} disabled={!isValid} />

Plain (non-PCI) inputs collected with a regular React Native TextInput — such as a billing
state dropdown — are not tracked by useVaultFormValidity. Gate your submit button on
isValid and your own checks for any plain inputs.

useVaultFieldState(fieldName) — returns the current VGSTextInputState for one named
field, or undefined until the field first reports. Use it to show per-field validation
feedback. The state object contains non-sensitive metadata only: isValid, isDirty,
isEmpty, isFocused, and for card fields, last4, bin, and cardBrand. Raw field values
are never included.

const pan = useVaultFieldState(VAULT_FIELD_NAMES.pan);

{pan?.isDirty && !pan.isValid && (
  <Text>Please enter a valid card number.</Text>
)}

Submitting a payment method

For Coinme's payment-method endpoint specifically, the SDK ships a submitPaymentMethod helper
that assembles the nested request body for you. The card number, expiry, CVC, and billing
address fields are wired in securely by their VAULT_FIELD_NAMES; the remaining values
(customerId, paymentProcessAssociation, billing first/last name, etc.) are plain literals
you supply.

import {
  CoinmeCardNumberField,
  CoinmeExpiryField,
  CoinmeCvcField,
  CoinmeTextField,
  CoinmeVaultError,
  NotEmptyRule,
  submitPaymentMethod,
  useCoinmeVault,
  useVaultFormValidity,
  VAULT_FIELD_NAMES,
} from '@coinme-security/vault-sdk-react-native';
import { useState } from 'react';
import { Button, Text, TextInput, View } from 'react-native';

// Define validation rules outside the component so fields don't re-register on every render.
const streetRules = [new NotEmptyRule('Street address is required')];
const cityRules   = [new NotEmptyRule('City is required')];
const zipRules    = [new NotEmptyRule('ZIP code is required')];

const PaymentMethodForm = () => {
  const vault = useCoinmeVault();
  const { isValid } = useVaultFormValidity();
  const [state, setState] = useState('');
  const [status, setStatus] = useState('');

  const onSubmit = async () => {
    try {
      const result = await submitPaymentMethod(vault, {
        customerId: 'customer_123',
        paymentProcessAssociation: 'BUY',     // 'BUY' | 'SELL'
        paymentProviderId: 'provider_123',
        webSessionId: 'session_123',           // from the Coinme Risk SDK
        billingAddress: {
          firstName: 'Jane',
          lastName: 'Doe',
          state,                               // collected via plain TextInput below
          country: 'US',                       // optional; defaults to 'US'
        },
        headers: {
          'Content-Type': 'application/json',
          Authorization: 'Bearer <token>',
          'X-Device-Fingerprint': '<fingerprint>',
        },
      });
      setStatus(`OK ${result.status}`);
    } catch (error) {
      if (error instanceof CoinmeVaultError) {
        setStatus(`${error.kind}: ${error.message}`);
      }
    }
  };

  return (
    <View>
      <CoinmeCardNumberField fieldName={VAULT_FIELD_NAMES.pan} placeholder="Card number" />
      <CoinmeExpiryField     fieldName={VAULT_FIELD_NAMES.expiry} placeholder="MM/YY" />
      <CoinmeCvcField        fieldName={VAULT_FIELD_NAMES.cvc} placeholder="CVC" />
      <CoinmeTextField fieldName={VAULT_FIELD_NAMES.address1} placeholder="Street address"
        validationRules={streetRules} />
      <CoinmeTextField fieldName={VAULT_FIELD_NAMES.address2} placeholder="Apt / Suite (optional)" />
      <CoinmeTextField fieldName={VAULT_FIELD_NAMES.city} placeholder="City"
        validationRules={cityRules} />
      <CoinmeTextField fieldName={VAULT_FIELD_NAMES.zipcode} placeholder="ZIP code"
        validationRules={zipRules} />

      {/* Billing state is not PCI-sensitive — collect it with a plain TextInput. */}
      <TextInput value={state} onChangeText={setState} placeholder="State (e.g. WA)" />

      <Button
        title="Submit"
        onPress={onSubmit}
        disabled={!isValid || state.trim() === ''}
      />
      <Text>{status}</Text>
    </View>
  );
};

submitPaymentMethod input fields

FieldTypeNotes
customerIdstringThe signed-in Coinme customer the payment method belongs to.
paymentProcessAssociation'BUY' | 'SELL'Whether the card is being added for a buy or sell ramp.
paymentProviderIdstringPayment provider identifier; sent as providerId in the request body.
webSessionIdstringRisk SDK web session identifier (from the Coinme Risk SDK). Always required.
billingAddressobject{ firstName, lastName, state, country? } — plain literals. country defaults to 'US'.
headersRecord<string, string>?Optional request headers, e.g. Authorization, X-Device-Fingerprint.

The card fields (pan, cvc, expiry) and billing fields (address1, address2, city,
zipcode) are assembled into the request body using their VAULT_FIELD_NAMES constants. Every
submit auto-attaches Coinme distributed-tracing headers (X-B3-TraceId, X-B3-SpanId); pass
your own values in headers to override them.


Error handling

Every failure during submit — VGS field validation, network error, HTTP error, or a Coinme
application-level error — is normalized to a CoinmeVaultError. Catch it and branch on
error.kind:

try {
  await vault.submit();
} catch (error) {
  if (error instanceof CoinmeVaultError) {
    switch (error.kind) {
      case 'validation':
        // One or more fields failed VGS validation before the request was sent.
        showFieldErrors(error.fields); // Record<string, string[]>
        break;
      case 'server':
        // The request completed with a non-2xx HTTP status.
        reportStatus(error.status);
        break;
      case 'caas':
        // 2xx response, but Coinme's backend returned an application error envelope.
        reportCaas(error.errorResponse);
        break;
      case 'network':
        // The request never completed (offline, DNS failure, TLS error).
        showRetry();
        break;
      default:
        // 'unknown' — any other failure, including VGS misconfiguration.
        showRetry();
    }
  }
}
kindMeaningExtra fields
validationA field failed VGS validation before the request was sent.fieldsRecord<string, string[]> of VGS messages
serverThe request completed with a non-2xx status.status
caas2xx response carrying a Coinme application-level error envelope.status, errorResponse
networkThe request never completed (offline, DNS/TLS failure).
unknownAny other failure, including VGS misconfiguration.

CoinmeVaultError never contains raw card data. The original underlying error is always
available on error.cause.


Advanced topics

These sections cover less common integration scenarios. Most integrations won't need them.

Custom ingress or submit path

By default the SDK submits to the ingress host and path baked into the selected environment.
To override either for a given environment, pass a config prop to CoinmeVaultProvider. You
must supply entries for both sandbox and live; environment decides which is active. The
vault ID itself is never configurable at runtime.

<CoinmeVaultProvider
  environment="sandbox"
  config={{
    sandbox: {
      ingress: 'https://secure.sandbox.example.com',
      path: '/your/submit/path',
    },
    live: {
      ingress: 'https://secure.example.com',
      path: '/your/submit/path',
    },
  }}
>
  <CardForm />
</CoinmeVaultProvider>

Custom request body

When your ingress expects a body shape other than the built-in payment-method one, call
vault.submit() directly. Provide plain values in extraData and reference collected fields
via {{ fieldName }} placeholders in customRequestStructure — VGS resolves them securely at
submit time.

await vault.submit({
  extraData: { customerId: 'cust_123' },
  customRequestStructure: {
    card: {
      number: `{{ ${VAULT_FIELD_NAMES.pan} }}`,
      cvv:    `{{ ${VAULT_FIELD_NAMES.cvc} }}`,
    },
  },
  headers: { Authorization: `Bearer ${authToken}` },
});

Using a vault without the provider

If wrapping your form in CoinmeVaultProvider is not practical, you can construct the vault
directly with createCoinmeVault and pass it to each field and to submitPaymentMethod via the
vault prop or argument. You are responsible for calling vault.dispose() on unmount.

import { createCoinmeVault } from '@coinme-security/vault-sdk-react-native';
import { useMemo, useEffect } from 'react';

const PaymentForm = () => {
  const vault = useMemo(() => createCoinmeVault({ environment: 'sandbox' }), []);
  useEffect(() => () => vault.dispose(), [vault]);

  return (
    <>
      <CoinmeCardNumberField vault={vault} fieldName={VAULT_FIELD_NAMES.pan} />
      <CoinmeExpiryField vault={vault} fieldName={VAULT_FIELD_NAMES.expiry} />
      <CoinmeCvcField vault={vault} fieldName={VAULT_FIELD_NAMES.cvc} />
      <Button onPress={() => submitPaymentMethod(vault, input)} title="Submit" />
    </>
  );
};

Note that useVaultFormValidity() and useVaultFieldState() depend on the provider's field
registry. Without a provider, track validity yourself using each field's onStateChange prop.

Raw VGS access

The SDK re-exports the entire @vgs/collect-react-native public API, so VGS primitives —
validation rules like NotEmptyRule, VGSError, VGSErrorCode, VGSTextInputState, and
serializers — are importable directly from this package:

import { NotEmptyRule, VGSError } from '@coinme-security/vault-sdk-react-native';

For their full API, see the
VGS Collect React Native documentation.


Security rules

These rules apply to your integration. They carry through directly from VGS:

  • Never log or persist raw card values. Use only the state metadata surfaced by
    VGSTextInputState (e.g. isValid, isDirty, last4, cardBrand) and the parsed
    response body. The raw PAN, CVC, and expiry are never accessible to your code — keep it
    that way.
  • Always gate submit on validity. Use useVaultFormValidity() to disable your submit
    button until isValid is true. This prevents a malformed card from ever being sent.
  • Import only from the package root. Never reach into @vgs/collect-react-native
    subpaths directly.

Example apps

Runnable reference implementations are included in the SDK repository:

  • examples/bare — bare React Native app with the full payment-method form.
  • examples/expo — Expo equivalent.

Both examples render a complete card + billing address form, gate submit on validity, and
branch CoinmeVaultError by kind.


Troubleshooting

401 / authentication error during installCLOUDSMITH_TEAM_TOKEN is missing or
expired. Re-export it and reinstall:

export CLOUDSMITH_TEAM_TOKEN=<your-token>
yarn install

Unable to resolve "@vgs/collect-react-native" — the peer dependency is not installed.
Add it explicitly:

yarn add @vgs/collect-react-native

CoinmeCardNumberField needs a vault / useCoinmeVault must be used within a CoinmeVaultProvider
— a field or hook is being rendered outside the provider. Wrap the relevant subtree in
<CoinmeVaultProvider environment="…">, or pass an explicit vault prop to the field.

Submit button never enablesuseVaultFormValidity() requires every registered field to
be valid. Check that each field has a fieldName, that fields with required input have
validationRules set, and that any plain TextInput values are validated separately (they do
not register with the vault and are not counted by isValid).

Metro can't resolve the SDK after install — clear Metro's cache:

# bare React Native
npx react-native start --reset-cache

# Expo
npx expo start --clear

Expo: secure fields don't render, or submit fails — confirm @vgs/collect-react-native is
on a 1.x release (pure JS). If your app requires native modules outside this SDK, you may
need a development build (npx expo prebuild) for those modules; the SDK itself runs
identically in Expo Go or a development build.