@bhut-it/expo-apple-wallet
v1.0.4
Published
A module to call apple wallet using Passkit API
Readme
expo-apple-wallet (ideal para projetos Expo gerenciados)
História
Criei essa biblioteca para simplificar a integração do encarteiramento da Apple Wallet em projetos que utilizam o Expo Managed Flow, onde não há possibilidade de mexer em código nativo. Usando a Expo Modules API, deu pra embutir a integração com o PassKit como um módulo Swift comum, que funciona com prebuild/EAS Build sem precisar de eject nem manter uma pasta ios/ própria no projeto consumidor.
- Somente iOS. O módulo Android é um stub sem nenhuma função (existe só pra não quebrar o autolinking). Sempre proteja as chamadas com
Platform.OS === "ios".
Compatibilidade
| Versão da lib | Testada com Expo SDK | Observações |
|---|---|---|
| 3.3.2+ | 57+ | Exige o fix de MainActor (ver Troubleshooting) — o expo-modules-core 57 mudou como o currentViewController() precisa ser chamado. |
| 3.3.1 e anteriores | até 56 | Funciona sem o MainActor. |
Se você atualizar o SDK do Expo e o botão de "adicionar à wallet" parar de funcionar silenciosamente (ou o app fechar sem erro no JS), olha a seção Troubleshooting antes de sair refazendo tudo do zero.
Instalação
npm install expo-apple-wallet
# ou
yarn add expo-apple-walletConfiguração
1. Painel da Apple Developer
Nas capabilities do seu identifier:

- Manda um e-mail pra Apple pedindo a liberação da capability "In-App Provisioning" pro seu identifier (não é self-service, a Apple aprova manualmente).
- Cria um provisioning profile do tipo "App Store Provisioning Profile".
2. Entitlements no app.config.js / app.config.ts
ios: {
entitlements: {
"com.apple.developer.payment-pass-provisioning": true,
"com.apple.developer.pass-type-identifiers": ["$(TeamIdentifierPrefix)*"],
},
},Referência da API
Todos os métodos ficam no export default: import ExpoAppleWallet from "expo-apple-wallet".
| Método | Assinatura | Descrição |
|---|---|---|
| isAvailable | () => Promise<boolean> | Se o provisioning do PassKit está disponível no device. Sempre retorna false no Simulador — teste em device real. |
| isCardAlreadyAdded | (panTokenSuffix: string) => Promise<boolean> | Se já existe um cartão com esse sufixo de PAN na wallet do usuário. |
| initEnrollProcess | (panTokenSuffix: string, holder: string) => Promise<{ nonce: string; nonceSignature: string; certificates: string } \| undefined> | Apresenta a tela nativa de adicionar à wallet e retorna o payload de criptografia que seu back-end precisa pra chamar o serviço web da Apple. |
| completeEnrollment | (activationData: string, ephemeralPublicKey: string, encryptedPassData: string) => void | Chame depois que seu back-end devolver os dados criptografados do pass, pra concluir o provisioning. A ordem dos parâmetros importa — ver exemplo abaixo. |
| inAppVerification | (serialNumber: string, passTypeIdentifier: string, action: string) => Promise<Record<string, string>> | Busca um pass já existente na wallet (local ou remoto) pra fluxos de ativação/verificação de cartão dentro do app. Retorna { error, message } se o pass não for encontrado ou não estiver em um estado ativável. |
O que é cada parâmetro
panTokenSuffix: os últimos dígitos (sufixo) do PAN/token do cartão que seu processador de pagamentos te dá — não é o número completo do cartão. A Apple usa isso só pra identificar/exibir qual cartão é, e pra checar se ele já foi adicionado antes (isCardAlreadyAdded).holder: o nome do titular do cartão, exatamente como deve aparecer na tela de "Adicionar à Wallet" (campo "Nome" da tela nativa).serialNumber/passTypeIdentifier: identificam um pass que já está instalado na Wallet do usuário (vêm do próprio pass, não são gerados por você). Usados só no fluxo deinAppVerification, pra achar esse pass e continuar a ativação dele.activationData/ephemeralPublicKey/encryptedPassData: os três valores que voltam do seu backend, depois que ele chama o web service da Apple usando ononce/nonceSignature/certificatesque oinitEnrollProcesste devolveu. São eles que fecham o ciclo de criptografia — sem os três,completeEnrollmentnão tem o que fazer.
Uso
Checando disponibilidade
const [isAvailable, setIsAvailable] = useState(false)
useEffect(() => {
ExpoAppleWallet.isAvailable().then(setIsAvailable)
}, [])Adicionando um cartão
const addCard = async () => {
const result = await ExpoAppleWallet.initEnrollProcess(panTokenSuffix, holder)
if (result) {
const { nonce, nonceSignature, certificates } = result
// Envia nonce/nonceSignature/certificates pro seu back-end,
// que chama o serviço web da Apple e devolve:
// { activationData, ephemeralPublicKey, encryptedPassData }
const { activationData, ephemeralPublicKey, encryptedPassData } =
await yourBackend.encryptNonce({ nonce, nonceSignature, certificates })
await ExpoAppleWallet.completeEnrollment(
activationData,
ephemeralPublicKey,
encryptedPassData,
)
}
}Resultado final
Depois de chamar initEnrollProcess e completar o fluxo com completeEnrollment, o usuário vê a tela nativa da Apple pra confirmar a adição do cartão à Wallet:
Testando mudanças nessa lib
Você não consegue validar uma mudança nativa completamente dentro do example/ desse repo — a entitlement de In-App Provisioning da Apple é vinculada a um Team ID/merchant e provisioning profile específicos, que o app de exemplo não tem. Teste contra um app consumidor real que já tenha a capability aprovada:
- Faça a mudança em
ios/ExpoAppleWalletModule.swift. - Copia o arquivo pra dentro do app consumidor:
cp ios/ExpoAppleWalletModule.swift <app>/node_modules/expo-apple-wallet/ios/ExpoAppleWalletModule.swift. - No app consumidor, roda
npx expo prebuild --cleanpra regenerar o projeto nativo com o código atualizado. - Abre
ios/*.xcworkspaceno Xcode e roda num device real (o Simulador não testa PassKit —canAddPaymentPass()sempre retornafalse). - Testa o fluxo completo de adicionar um cartão real à wallet.
- Só depois de passar nesse teste, leve a mudança de volta pra esse repo e faça o commit.
Troubleshooting
App fecha silenciosamente (sem erro no JS, nada no log do Metro) ao adicionar um cartão — depois de atualizar o Expo
Causa: o expo-modules-core mudou como o Utilities.currentViewController() resolve a key window entre o Expo SDK 56 e o 57. Os closures de AsyncFunction não necessariamente rodam na main thread, e mexer em UIKit (resolver/apresentar um view controller) fora da main thread pode derrubar o processo diretamente — antes mesmo de chegar num try/catch do JS ou numa rejeição de Promise. É por isso que parece "silencioso": o crash é nativo, não é uma exceção do JS.
Como confirmar: um try/catch no JS não vai mostrar nada. Você precisa do stack trace nativo:
- Conecta o device, abre
ios/*.xcworkspaceno Xcode, roda com o debugger conectado. - Reproduz o crash. Olha o stack trace no debugger — se algum frame apontar pra dentro de
Utilities.currentViewController()(ou outra chamada de UIKit) dentro doexpo-modules-core, é a mesma classe de bug.
Fix: envolva qualquer código que mexa em appContext?.utilities?.currentViewController(), PKAddPaymentPassViewController.canAddPaymentPass() ou .present(...) num try await MainActor.run { ... }, ao invés de chamar direto ou usar um DispatchQueue.main.async fire-and-forget (que além disso engole silenciosamente qualquer erro lançado dentro dele). Veja o initEnrollProcess em ios/ExpoAppleWalletModule.swift pra ver a implementação atual.
Regra geral pra futuras atualizações de SDK: qualquer coisa nesse módulo que mexa em UIKit (UIApplication, UIWindow, UIViewController, PKAddPaymentPassViewController, etc.) precisa rodar dentro de MainActor.run, nunca assumir que já está na main thread.
