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"]
andsessionArgs
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 sensitivesessionArgs
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