@column-na/card-reveal-react-native
Show a Column card's full number, CVV, expiry and cardholder name in your React Native or Expo app. The card data never touches your code or your servers.
It's pure JavaScript with no native dependencies, so it works in Expo Go, Expo development builds and bare React Native. It requires React 19.1 or later and React Native 0.81 or later.
How it works
- The app calls
client.prepareReveal(), which makes the reveal's one-time key and gives the app its thumbprint,keyThumbprint. Send it to your server with the card ID. - Your server creates a single-use reveal token, valid for one minute, bound to that key, using your API key:
Check that the signed-in user owns the card first. Then send only the tokencurl -X POST 'https://api.column.com/issuing/cards/<card_id>/reveal-tokens' -u :<YOUR API KEY> \ -H 'Content-Type: application/json' -d '{"reveal_key_thumbprint": "<keyThumbprint>"}' # {"id":"crtk_...","card_id":"card_...","expires_at":"...","reveal_key_thumbprint":"..."}idto the app. - The app renders a
CardRevealTextfor each field and callsprepared.reveal(...). The request goes straight from the device to Column's API, and the card details come back encrypted end to end to the one-time key, which stays in the SDK's memory. The values stay inside the SDK's components: no callback, ref or return value exposes them. The token works only with that key, so a token that leaks is useless to anyone else.
Install
npx expo install @column-na/card-reveal-react-native react-native-get-random-values
# or: npm install @column-na/card-reveal-react-native react-native-get-random-values
Every reveal, live or sandbox, generates a one-time key, which needs a secure random source (crypto.getRandomValues). React Native's engine doesn't provide one, so import the polyfill once, at the top of your app's entry file, before anything else:
import 'react-native-get-random-values';
Without it, reveal() throws invalid_options.
The polyfill is part of the reveal's trust boundary: the one-time key is only as unpredictable as the random bytes it gives the SDK. Use react-native-get-random-values, which installs crypto.getRandomValues backed by the operating system's secure random source, also in Expo apps, and don't replace crypto.getRandomValues with a shim or a test stub in a build that reveals real cards. The SDK checks the source before each key (it refuses all-zero or repeating output, output not written into the array it passed in, and a degenerate key) and fails the reveal with reveal_failed instead of sending a predictable key. The check inspects the very bytes the key is then made from, so a polyfill can't pass it with one set of bytes and seed the key with another, but it can't tell a sound source from one an attacker can reproduce.
Usage
import { useEffect, useMemo, useRef } from 'react';
import { View } from 'react-native';
import {
CardRevealError,
CardRevealText,
createCardRevealClient,
type CardRevealTextHandle,
} from '@column-na/card-reveal-react-native';
export function CardDetails({
cardId,
createRevealToken, // asks your server for a token bound to the key: (keyThumbprint) => Promise<string>
}: {
cardId: string;
createRevealToken: (keyThumbprint: string) => Promise<string>;
}) {
const client = useMemo(() => createCardRevealClient({ environment: 'live' }), []); // or 'sandbox'
const pan = useRef<CardRevealTextHandle>(null);
const cvv2 = useRef<CardRevealTextHandle>(null);
const month = useRef<CardRevealTextHandle>(null);
const year = useRef<CardRevealTextHandle>(null);
useEffect(() => {
let cancelled = false;
let unmount = () => {};
(async () => {
// One prepared key and one token per reveal: both are single use.
const prepared = await client.prepareReveal();
const revealToken = await createRevealToken(prepared.keyThumbprint);
if (cancelled) return;
const reveal = prepared.reveal({
cardId,
revealToken,
fields: { pan, cvv2, expiration_month: month, expiration_year: year },
style: { color: '#fff', fontFamily: 'Menlo' },
fieldStyles: { pan: { fontSize: 20 } },
format: { pan: 'spaced', expirationYear: 'YY' },
});
unmount = reveal.unmount;
await reveal.ready;
})().catch((error) => {
if (CardRevealError.is(error)) console.warn('Card reveal failed:', error.code);
});
return () => {
cancelled = true;
unmount();
};
}, [client, cardId, createRevealToken]);
return (
<View>
<CardRevealText ref={pan} placeholder="•••• •••• •••• ••••" selectable />
<CardRevealText ref={cvv2} placeholder="•••" />
<CardRevealText ref={month} placeholder="MM" />
<CardRevealText ref={year} placeholder="YY" />
</View>
);
}
React bindings
CardRevealProvider and CardRevealSlot do the same in a few lines:
import { CardRevealProvider, CardRevealSlot } from '@column-na/card-reveal-react-native';
<CardRevealProvider
client={client}
cardId={cardId}
mintToken={({ cardId, keyThumbprint }) => myServer.createRevealToken(cardId, keyThumbprint)}
>
<CardRevealSlot field="pan" placeholder="•••• •••• •••• ••••" selectable />
<CardRevealSlot field="cvv2" placeholder="•••" />
</CardRevealProvider>;
mintToken({ cardId, keyThumbprint })asks your server for a token bound to the reveal's key and resolves with itsid. It's called once per reveal.<CardRevealSlot field="...">is aCardRevealTextfor that field and takes its props.<CardRevealProvider>also takesstyle,fieldStyles,format,timeoutMsandenabled(defaulttrue;falseholds off, or hides a shown card).- Inside it,
useCardRevealState()returns{ status, error, retry }.statusis'idle','preparing','minting','revealing','ready'or'error'.erroris aCardRevealErroror whatevermintTokenthrew.retry()starts over with a new key and token. - Changing the client, card, slots, styles,
formatortimeoutMsreveals again with a new token; unmounting the provider unmounts the reveal and forgets any unused key. useCardReveal({ client, cardId, mintToken, fields })is the same as a hook, withfieldsmapping each field to aCardRevealTextref.
Key-bound tokens: prepareReveal()
client.prepareReveal() makes the next reveal's one-time P-256 key pair before the token exists. It resolves with:
keyThumbprint: the key's RFC 7638 SHA-256 thumbprint, base64url without padding (43 characters). Pass it to your server, which mints the token with it asreveal_key_thumbprint. It's a hash of a public key, not a secret.reveal(options): reveals with that key, with the same options and result asclient.reveal(). It may be called once; a second call throwsinvalid_options. Invalid options throw before the key is used, so a corrected call can still use it.discard(): forgets the key without revealing, for when the reveal won't happen (the token couldn't be minted, or the screen moved on). Afterwardsreveal()throwsinvalid_options.
const prepared = await client.prepareReveal();
const revealToken = await myServer.createRevealToken(cardId, prepared.keyThumbprint);
const { ready, unmount } = prepared.reveal({ cardId, revealToken, fields: { pan, cvv2 } });
Column answers a bound token only for a reveal that sends that key. Any other key gets a 401 (reveal_failed), and the token isn't used up. The private key stays in the SDK's memory and is forgotten once the reveal's request settles, or when the reveal is torn down. prepareReveal() rejects with invalid_options (no secure random source, or live on react-native-web) or reveal_failed.
Every reveal token is minted with a reveal_key_thumbprint, so every reveal starts with prepareReveal(). client.reveal() takes the same options, but a token minted for another key, or a reveal made without the prepared key, gets a 401.
CardRevealText
| Prop | Description |
|---|---|
placeholder | Shown before the reveal and after unmount. |
style | Text style. The reveal's style and fieldStyles apply on top. |
selectable | Whether the user can select (and copy) the revealed text. Default false. |
testID | Test identifier. |
Options
| Option | Description |
|---|---|
cardId | The card's ID (card_...). |
revealToken | A reveal token ID (crtk_...) from your server. Single use, valid for 1 minute. |
fields | Maps any of pan, cvv2, cardholder_name, expiration_month, expiration_year and pin to a CardRevealText ref. Use a different component for each field; see Revealing the PIN. |
style / fieldStyles | React Native text styles for every field, or for one field. |
format.pan | 'spaced' (default: 4111 1111 1111 1111 for 16-digit numbers) or 'plain'. |
format.expirationYear | 'YYYY' (default) or 'YY'. |
timeoutMs | How long ready waits before rejecting with timeout. Default 15000, maximum 120000. |
hidden | true reveals the fields masked (••••) until you call show(). Default false. |
autoHideMs | Unmounts the reveal this many milliseconds after ready resolves (1 to 3600000). |
onAutoHide | Called once when autoHideMs unmounts the reveal. |
ready, unmount, hide and show
readyresolves once every field shows its value.- If any field fails, or the timeout passes,
readyrejects and every field returns to its placeholder. unmount()does the same. Ifreadyis still pending, it rejects withunmounted.hide()andshow()mask and unmask every field; see Hide, show and copy.- Invalid options throw
invalid_optionssynchronously.
Hide, show and copy
reveal.hide()masks every field in place (each character but spaces shows as•) andreveal.show()shows it again, without a new token or request. A maskedCardRevealTextisn't selectable. Both do nothing afterunmount()or a failure.hidden: truestarts masked.autoHideMsremoves the values that long afterready, asunmount()does (the placeholders come back), and callsonAutoHideonce. Showing them again needs a new token.- Copy: React Native has no clipboard API without a native module (
@react-native-clipboard/clipboardorexpo-clipboard), and this SDK has no native code, so it doesn't offer a copy button.copyableandonCopyare web only and throwinvalid_optionshere. SetselectableonCardRevealTextso the cardholder can copy with the system text selection.
Errors
Every error is a CardRevealError with a stable code: invalid_options, container_not_found (a ref isn't attached to a mounted CardRevealText), reveal_failed, timeout and unmounted. field and the HTTP status are set when known, and apiCode when Column's API returned an error code. A 401 means the token is missing, invalid, already used, expired, for a different card or bound to a different reveal key. Messages never contain the token or card data, and the SDK never logs either.
Revealing the PIN
Add pin to fields to show a debit card's PIN. The SDK asks Column only for the fields you list, so the PIN is returned only when you include pin, and a PIN-only reveal doesn't return the card number or CVV. Your server creates reveal tokens the same way either way. The card's program must have PIN reveal enabled. Verify the cardholder before you reveal their PIN.
Reveal pin on its own rather than alongside the card number. If the PIN can't be revealed, the whole reveal fails, so asking for it with other fields would hide those too. When it fails, reveal_failed carries Column's error code in apiCode:
apiCode | Meaning |
|---|---|
pin_not_revealable | The PIN was set before PIN reveal was available. Set the PIN again to make it revealable. |
feature_not_enabled | PIN reveal isn't enabled for the card's program. |
invalid_field_value | The card has no PIN, or isn't a debit card. |
These failures happen before the reveal token is used, so the same token still works for a reveal without pin.
Sandbox and live
| Live | Sandbox | |
|---|---|---|
| Data | Real cards | Simulated cards |
| Request | Column directly: GET https://api.column.com/issuing/cards/{id}/reveal with Live-Mode: Yes, encrypted end to end | The same request with Live-Mode: No, also encrypted |
Both render into the same CardRevealText components. In sandbox, sandboxBaseUrl can point at a local mock. Use http://127.0.0.1:<port> on the iOS simulator and http://10.0.2.2:<port> on the Android emulator.
Live reveal isn't available on React Native Web, where it throws invalid_options. On the web, use @column-na/card-reveal, which keeps card data in isolated iframes.
Versioning
This package follows semantic versioning and releases at the same version as the web SDK. Every request sends X-Column-SDK-Version: card-reveal-react-native/<version>.
The SDK sends no analytics.
License
Proprietary. Use is subject to the Column Card Reveal SDK License in LICENSE and requires an agreement with Column.