Skip to main content

React Native — Bill History and Payment Methods (Verizon example)

This guide shows how to build a React Native flow to:

  • Fetch billing history from Verizon
  • Add a new payment method using products: ["card-switching"] and sessionArgs

Prerequisites

  • A Passage app with a Verizon integration configured
  • Backend able to call Passage APIs securely (API key stored server-side)
  • React Native project set up; install:

Installation

npm install @getpassage/react-native react-native-webview @react-native-cookies/cookies

IOS Setup

cd ios && pod install # iOS

Backend: Generate intent tokens

Create endpoints that mint short-lived intent tokens for the client. The backend controls integrationId, products, and sessionArgs.

Example (Node/Express, TypeScript-ish pseudocode):

import express from 'express';
import fetch from 'node-fetch';

const PASSAGE_API_KEY = process.env.PASSAGE_API_KEY!; // Keep secret
const PASSAGE_APP_ID = process.env.PASSAGE_APP_ID!;

// Helper to call Passage to create an intent token
async function createIntentToken(payload: Record<string, unknown>) {
const res = await fetch(`https://api.passage.id/v1/apps/${PASSAGE_APP_ID}/intent_token`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${PASSAGE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Passage intent token error ${res.status}: ${text}`);
}
return res.json(); // { intentToken: string, ... }
}

const app = express();
app.use(express.json());

// Issue token to fetch Verizon billing history
app.post('/api/passage/intent/billing-history', async (req, res) => {
try {
// Identify your user/session here and authorize access
const userId = req.body.userId; // your app's user id (optional for your model)

const payload = {
integrationId: 'verizon',
products: ['billing-history'],
// Any server-controlled options can go here
// sessionArgs can also be provided if needed by your integration
// userId could be included if your backend maps it
};
const token = await createIntentToken(payload);
res.json({ intentToken: token.intentToken || token.intent_token || token.token });
} catch (e: any) {
res.status(500).json({ error: e.message });
}
});

// Issue token to add a new payment method via card switching
app.post('/api/passage/intent/card-switching', async (req, res) => {
try {
const { userId, sessionArgs } = req.body;
const payload = {
integrationId: 'verizon',
products: ['card-switching'],
sessionArgs: {
// Example values; set server-side based on your PCI policy and UX
// cardNumber, expirationDate, cvv, billingZip can be passed if collected server-side
...sessionArgs,
},
};
const token = await createIntentToken(payload);
res.json({ intentToken: token.intentToken || token.intent_token || token.token });
} catch (e: any) {
res.status(500).json({ error: e.message });
}
});

app.listen(3000, () => console.log('Backend up on :3000'));

Notes:

  • Do not expose PASSAGE_API_KEY to the client.
  • The exact intent token response key can vary; return a normalized intentToken to the client.
  • Keep integrationId, products, and any sensitive sessionArgs server-controlled.

React Native setup

Wrap your app with the provider:

import { PassageProvider } from '@getpassage/react-native';

export function App() {
return (
<PassageProvider config={{ debug: false }}>
<RootNavigator />
</PassageProvider>
);
}

React Native: Fetch Verizon billing history

The client only requests an intent token from your backend, then opens Passage with that token. Handle completion events to read data.

import React from 'react';
import { Button, View } from 'react-native';
import { usePassage } from '@getpassage/react-native';

async function getBillingHistoryToken() {
const res = await fetch('https://your.api.example.com/api/passage/intent/billing-history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'current-user-id' }),
});
const { intentToken } = await res.json();
return intentToken;
}

export function VerizonBillingHistoryButton({ onData }: { onData?: (bills: any[], account?: any) => void }) {
const passage = usePassage();

const handlePress = async () => {
const intentToken = await getBillingHistoryToken();

await passage.initialize({
onConnectionComplete: () => {
// Connection succeeded
},
onDataComplete: (data: any) => {
// Supports legacy array or { data: { history, accountInfo } }
let bills: any[] = [];
let accountInfo: any | undefined;
if (data?.data) {
if (Array.isArray(data.data)) bills = data.data;
else if (typeof data.data === 'object') {
if (Array.isArray(data.data.history)) {
bills = data.data.history;
accountInfo = data.data.accountInfo;
}
}
}
onData?.(bills, accountInfo);
},
onError: (err: any) => {
console.error('Passage error', err);
},
onExit: () => {},
});

await passage.open({ intentToken });
};

return (
<View>
<Button title="Load Verizon Bills" onPress={handlePress} />
</View>
);
}

React Native: Add payment method (card switching)

Collect inputs as needed, but send them to your backend so it can embed them into the intent sessionArgs. The client still only receives a token.

import React, { useState } from 'react';
import { Button, TextInput, View } from 'react-native';
import { usePassage } from '@getpassage/react-native';

async function getCardSwitchingToken(sessionArgs: any) {
const res = await fetch('https://your.api.example.com/api/passage/intent/card-switching', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'current-user-id', sessionArgs }),
});
const { intentToken } = await res.json();
return intentToken;
}

export function AddPaymentMethod() {
const passage = usePassage();
const [cardNumber, setCardNumber] = useState('');
const [expirationDate, setExpirationDate] = useState(''); // MM/YYYY
const [cvv, setCvv] = useState('');
const [billingZip, setBillingZip] = useState('');

const handleAdd = async () => {
const intentToken = await getCardSwitchingToken({
cardNumber,
expirationDate,
cvv,
billingZip,
});

await passage.initialize({
onConnectionComplete: () => {},
onDataComplete: () => {
// Optionally refetch account info from your backend
},
onError: (e: any) => console.error(e),
onExit: () => {},
});

await passage.open({ intentToken });
};

return (
<View>
<TextInput placeholder="Card Number" value={cardNumber} onChangeText={setCardNumber} />
<TextInput placeholder="MM/YYYY" value={expirationDate} onChangeText={setExpirationDate} />
<TextInput placeholder="CVV" value={cvv} onChangeText={setCvv} secureTextEntry />
<TextInput placeholder="ZIP" value={billingZip} onChangeText={setBillingZip} />
<Button title="Add Payment Method" onPress={handleAdd} />
</View>
);
}

Data model compatibility

When handling onDataComplete, you may receive either a legacy array of bills or a new object shape:

// Legacy: data = { data: Bill[] }
// New: data = { data: { history: Bill[], accountInfo?: { paymentMethods?: Array<{ name: string; mask: string }> } } }

Normalize in the UI as shown above.

Production checklist

  • Keep all Passage configuration and API keys in the backend
  • Only pass short-lived intent tokens to the client
  • Validate user authorization before minting tokens
  • Log and monitor onError and backend failures