@bhut-it/expo-apple-wallet 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +5 -0
- package/README.md +136 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/applewallet/ExpoAppleWalletModule.kt +11 -0
- package/assets/add-to-wallet-result.png +0 -0
- package/assets/provisioning.png +0 -0
- package/assets/wallet.png +0 -0
- package/build/ExpoAppleWalletModule.d.ts +3 -0
- package/build/ExpoAppleWalletModule.d.ts.map +1 -0
- package/build/ExpoAppleWalletModule.js +4 -0
- package/build/ExpoAppleWalletModule.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +2 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +14 -0
- package/ios/ExpoAppleWallet.podspec +31 -0
- package/ios/ExpoAppleWalletModule.swift +240 -0
- package/package.json +40 -0
- package/src/ExpoAppleWalletModule.ts +5 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +9 -0
package/.eslintrc.js
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# expo-apple-wallet (ideal para projetos Expo gerenciados)
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/expo-apple-wallet)
|
|
4
|
+
[](https://www.npmjs.com/package/expo-apple-wallet)
|
|
5
|
+
|
|
6
|
+
## História
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
- **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"`.
|
|
11
|
+
|
|
12
|
+
## Compatibilidade
|
|
13
|
+
|
|
14
|
+
| Versão da lib | Testada com Expo SDK | Observações |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| 3.3.2+ | 57+ | Exige o fix de `MainActor` (ver [Troubleshooting](#troubleshooting)) — o `expo-modules-core` 57 mudou como o `currentViewController()` precisa ser chamado. |
|
|
17
|
+
| 3.3.1 e anteriores | até 56 | Funciona sem o `MainActor`. |
|
|
18
|
+
|
|
19
|
+
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](#troubleshooting) antes de sair refazendo tudo do zero.
|
|
20
|
+
|
|
21
|
+
## Instalação
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install expo-apple-wallet
|
|
25
|
+
# ou
|
|
26
|
+
yarn add expo-apple-wallet
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuração
|
|
30
|
+
|
|
31
|
+
### 1. Painel da Apple Developer
|
|
32
|
+
|
|
33
|
+
Nas capabilities do seu identifier:
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
1. 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).
|
|
39
|
+
2. Cria um provisioning profile do tipo **"App Store Provisioning Profile"**.
|
|
40
|
+
|
|
41
|
+
### 2. Entitlements no `app.config.js` / `app.config.ts`
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
ios: {
|
|
45
|
+
entitlements: {
|
|
46
|
+
"com.apple.developer.payment-pass-provisioning": true,
|
|
47
|
+
"com.apple.developer.pass-type-identifiers": ["$(TeamIdentifierPrefix)*"],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Referência da API
|
|
53
|
+
|
|
54
|
+
Todos os métodos ficam no export default: `import ExpoAppleWallet from "expo-apple-wallet"`.
|
|
55
|
+
|
|
56
|
+
| Método | Assinatura | Descrição |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `isAvailable` | `() => Promise<boolean>` | Se o provisioning do PassKit está disponível no device. Sempre retorna `false` no Simulador — teste em device real. |
|
|
59
|
+
| `isCardAlreadyAdded` | `(panTokenSuffix: string) => Promise<boolean>` | Se já existe um cartão com esse sufixo de PAN na wallet do usuário. |
|
|
60
|
+
| `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. |
|
|
61
|
+
| `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. |
|
|
62
|
+
| `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. |
|
|
63
|
+
|
|
64
|
+
### O que é cada parâmetro
|
|
65
|
+
|
|
66
|
+
- **`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`).
|
|
67
|
+
- **`holder`**: o nome do titular do cartão, exatamente como deve aparecer na tela de "Adicionar à Wallet" (campo "Nome" da tela nativa).
|
|
68
|
+
- **`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 de `inAppVerification`, pra achar esse pass e continuar a ativação dele.
|
|
69
|
+
- **`activationData` / `ephemeralPublicKey` / `encryptedPassData`**: os três valores que voltam do **seu backend**, depois que ele chama o web service da Apple usando o `nonce`/`nonceSignature`/`certificates` que o `initEnrollProcess` te devolveu. São eles que fecham o ciclo de criptografia — sem os três, `completeEnrollment` não tem o que fazer.
|
|
70
|
+
|
|
71
|
+
## Uso
|
|
72
|
+
|
|
73
|
+
### Checando disponibilidade
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
const [isAvailable, setIsAvailable] = useState(false)
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
ExpoAppleWallet.isAvailable().then(setIsAvailable)
|
|
80
|
+
}, [])
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Adicionando um cartão
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
const addCard = async () => {
|
|
87
|
+
const result = await ExpoAppleWallet.initEnrollProcess(panTokenSuffix, holder)
|
|
88
|
+
|
|
89
|
+
if (result) {
|
|
90
|
+
const { nonce, nonceSignature, certificates } = result
|
|
91
|
+
|
|
92
|
+
// Envia nonce/nonceSignature/certificates pro seu back-end,
|
|
93
|
+
// que chama o serviço web da Apple e devolve:
|
|
94
|
+
// { activationData, ephemeralPublicKey, encryptedPassData }
|
|
95
|
+
const { activationData, ephemeralPublicKey, encryptedPassData } =
|
|
96
|
+
await yourBackend.encryptNonce({ nonce, nonceSignature, certificates })
|
|
97
|
+
|
|
98
|
+
await ExpoAppleWallet.completeEnrollment(
|
|
99
|
+
activationData,
|
|
100
|
+
ephemeralPublicKey,
|
|
101
|
+
encryptedPassData,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Resultado final
|
|
108
|
+
|
|
109
|
+
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:
|
|
110
|
+
|
|
111
|
+
<img src="/assets/add-to-wallet-result.png" alt="Tela nativa de adicionar cartão ao Apple Pay" width="320" />
|
|
112
|
+
|
|
113
|
+
## Testando mudanças nessa lib
|
|
114
|
+
|
|
115
|
+
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:
|
|
116
|
+
|
|
117
|
+
1. Faça a mudança em `ios/ExpoAppleWalletModule.swift`.
|
|
118
|
+
2. Copia o arquivo pra dentro do app consumidor: `cp ios/ExpoAppleWalletModule.swift <app>/node_modules/expo-apple-wallet/ios/ExpoAppleWalletModule.swift`.
|
|
119
|
+
3. No app consumidor, roda `npx expo prebuild --clean` pra regenerar o projeto nativo com o código atualizado.
|
|
120
|
+
4. Abre `ios/*.xcworkspace` no Xcode e roda num **device real** (o Simulador não testa PassKit — `canAddPaymentPass()` sempre retorna `false`).
|
|
121
|
+
5. Testa o fluxo completo de adicionar um cartão real à wallet.
|
|
122
|
+
6. Só depois de passar nesse teste, leve a mudança de volta pra esse repo e faça o commit.
|
|
123
|
+
|
|
124
|
+
## Troubleshooting
|
|
125
|
+
|
|
126
|
+
### App fecha silenciosamente (sem erro no JS, nada no log do Metro) ao adicionar um cartão — depois de atualizar o Expo
|
|
127
|
+
|
|
128
|
+
**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.
|
|
129
|
+
|
|
130
|
+
**Como confirmar:** um `try/catch` no JS **não** vai mostrar nada. Você precisa do stack trace nativo:
|
|
131
|
+
1. Conecta o device, abre `ios/*.xcworkspace` no Xcode, roda com o debugger conectado.
|
|
132
|
+
2. Reproduz o crash. Olha o stack trace no debugger — se algum frame apontar pra dentro de `Utilities.currentViewController()` (ou outra chamada de UIKit) dentro do `expo-modules-core`, é a mesma classe de bug.
|
|
133
|
+
|
|
134
|
+
**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.
|
|
135
|
+
|
|
136
|
+
**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.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'expo.modules.applewallet'
|
|
4
|
+
version = '0.1.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useExpoPublishing()
|
|
11
|
+
|
|
12
|
+
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
13
|
+
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
14
|
+
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
15
|
+
def useManagedAndroidSdkVersions = false
|
|
16
|
+
if (useManagedAndroidSdkVersions) {
|
|
17
|
+
useDefaultAndroidSdkVersions()
|
|
18
|
+
} else {
|
|
19
|
+
buildscript {
|
|
20
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
21
|
+
ext.safeExtGet = { prop, fallback ->
|
|
22
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
project.android {
|
|
26
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
29
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "expo.modules.applewallet"
|
|
36
|
+
defaultConfig {
|
|
37
|
+
versionCode 1
|
|
38
|
+
versionName "0.1.0"
|
|
39
|
+
}
|
|
40
|
+
lintOptions {
|
|
41
|
+
abortOnError false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package expo.modules.applewallet
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
|
|
6
|
+
class ExpoAppleWalletModule : Module() {
|
|
7
|
+
override fun definition() = ModuleDefinition {
|
|
8
|
+
Name("ExpoAppleWallet")
|
|
9
|
+
|
|
10
|
+
}
|
|
11
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAppleWalletModule.d.ts","sourceRoot":"","sources":["../src/ExpoAppleWalletModule.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,qBAAqB,KAAyC,CAAC;AAErE,eAAe,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAppleWalletModule.js","sourceRoot":"","sources":["../src/ExpoAppleWalletModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,MAAM,qBAAqB,GAAG,mBAAmB,CAAC,iBAAiB,CAAC,CAAC;AAErE,eAAe,qBAAqB,CAAC","sourcesContent":["import { requireNativeModule } from 'expo-modules-core';\n\nconst ExpoAppleWalletModule = requireNativeModule('ExpoAppleWallet');\n\nexport default ExpoAppleWalletModule;\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC","sourcesContent":["export { default } from './ExpoAppleWalletModule';"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ExpoAppleWallet'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = {
|
|
14
|
+
:ios => '12.0',
|
|
15
|
+
:tvos => '12.0'
|
|
16
|
+
}
|
|
17
|
+
s.swift_version = '5.4'
|
|
18
|
+
|
|
19
|
+
s.source = {
|
|
20
|
+
:http => "https://registry.npmjs.org/@bhut-it/expo-apple-wallet/-/expo-apple-wallet-#{s.version}.tgz"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
s.dependency 'ExpoModulesCore'
|
|
24
|
+
|
|
25
|
+
# Swift/Objective-C compatibility
|
|
26
|
+
s.pod_target_xcconfig = {
|
|
27
|
+
'DEFINES_MODULE' => 'YES',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
31
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
import PassKit
|
|
4
|
+
|
|
5
|
+
private struct Card {
|
|
6
|
+
let panTokenSuffix: String
|
|
7
|
+
let holder: String
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
struct NonceResult {
|
|
11
|
+
let nonce: String
|
|
12
|
+
let nonceSignature: String
|
|
13
|
+
let certificates: [String]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private func isPassKitAvailable() -> Bool {
|
|
17
|
+
return PKAddPaymentPassViewController.canAddPaymentPass()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private func cardInformation(panTokenSuffix: String, holder: String) -> Card {
|
|
21
|
+
return Card(panTokenSuffix: panTokenSuffix, holder: holder)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class HandleDelegate: NSObject, PKAddPaymentPassViewControllerDelegate {
|
|
25
|
+
private let configuration: PKAddPaymentPassRequestConfiguration
|
|
26
|
+
private unowned let module: ExpoAppleWalletModule
|
|
27
|
+
private var completionHandler: ((PKAddPaymentPassRequest) -> Void)?
|
|
28
|
+
private var nonceContinuation: CheckedContinuation<NonceResult, Error>?
|
|
29
|
+
|
|
30
|
+
init(configuration: PKAddPaymentPassRequestConfiguration, module: ExpoAppleWalletModule) {
|
|
31
|
+
self.configuration = configuration
|
|
32
|
+
self.module = module
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func addPaymentPassViewController(
|
|
36
|
+
_ controller: PKAddPaymentPassViewController,
|
|
37
|
+
generateRequestWithCertificateChain certificates: [Data],
|
|
38
|
+
nonce: Data, nonceSignature: Data,
|
|
39
|
+
completionHandler handler: @escaping (PKAddPaymentPassRequest) -> Void
|
|
40
|
+
) {
|
|
41
|
+
self.completionHandler = handler
|
|
42
|
+
|
|
43
|
+
let nonceBase64 = nonce.base64EncodedString()
|
|
44
|
+
let nonceSignatureBase64 = nonceSignature.base64EncodedString()
|
|
45
|
+
let certificatesBase64 = certificates.map { $0.base64EncodedString() }
|
|
46
|
+
|
|
47
|
+
if let continuation = self.nonceContinuation {
|
|
48
|
+
continuation.resume(returning: NonceResult(
|
|
49
|
+
nonce: nonceBase64,
|
|
50
|
+
nonceSignature: nonceSignatureBase64,
|
|
51
|
+
certificates: certificatesBase64
|
|
52
|
+
))
|
|
53
|
+
self.nonceContinuation = nil
|
|
54
|
+
} else {
|
|
55
|
+
print("Nenhuma continuation esperando pelo nonce")
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func addPaymentPassViewController(
|
|
60
|
+
_ controller: PKAddPaymentPassViewController,
|
|
61
|
+
didFinishAdding pass: PKPaymentPass?,
|
|
62
|
+
error: Error?
|
|
63
|
+
) {
|
|
64
|
+
if let error = error {
|
|
65
|
+
print("Erro ao adicionar cartão: \(error.localizedDescription)")
|
|
66
|
+
} else {
|
|
67
|
+
print("Cartão adicionado com sucesso!")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
DispatchQueue.main.async {
|
|
71
|
+
controller.dismiss(animated: true, completion: nil)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func createEnrollViewController() -> PKAddPaymentPassViewController? {
|
|
76
|
+
return PKAddPaymentPassViewController(requestConfiguration: configuration, delegate: self)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func continueEnrollment(
|
|
80
|
+
activationData: Data,
|
|
81
|
+
ephemeralPublicKey: Data,
|
|
82
|
+
encryptedPassData: Data
|
|
83
|
+
) {
|
|
84
|
+
guard let handler = self.completionHandler else {
|
|
85
|
+
print("Handler não encontrado")
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let request = PKAddPaymentPassRequest()
|
|
90
|
+
|
|
91
|
+
request.activationData = activationData
|
|
92
|
+
request.ephemeralPublicKey = ephemeralPublicKey
|
|
93
|
+
request.encryptedPassData = encryptedPassData
|
|
94
|
+
|
|
95
|
+
handler(request)
|
|
96
|
+
|
|
97
|
+
self.completionHandler = nil
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
func waitForNonce() async throws -> NonceResult {
|
|
101
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
102
|
+
self.nonceContinuation = continuation
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public class ExpoAppleWalletModule: Module {
|
|
108
|
+
static var shared: ExpoAppleWalletModule?
|
|
109
|
+
var activeDelegate: HandleDelegate?
|
|
110
|
+
|
|
111
|
+
public func definition() -> ModuleDefinition {
|
|
112
|
+
Name("ExpoAppleWallet")
|
|
113
|
+
|
|
114
|
+
Function("isAvailable") { () -> Bool in
|
|
115
|
+
return isPassKitAvailable()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
AsyncFunction("isCardAlreadyAdded") { (panTokenSuffix: String) -> Bool in
|
|
119
|
+
let passLibrary = PKPassLibrary()
|
|
120
|
+
let passes = passLibrary.passes()
|
|
121
|
+
|
|
122
|
+
for pass in passes {
|
|
123
|
+
if let paymentPass = pass as? PKPaymentPass {
|
|
124
|
+
if paymentPass.primaryAccountNumberSuffix == panTokenSuffix {
|
|
125
|
+
return true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
AsyncFunction("initEnrollProcess") { (panTokenSuffix: String, holder: String) async throws -> [String: String] in
|
|
134
|
+
let card = cardInformation(panTokenSuffix: panTokenSuffix, holder: holder)
|
|
135
|
+
|
|
136
|
+
guard let configuration = PKAddPaymentPassRequestConfiguration(encryptionScheme: .ECC_V2) else {
|
|
137
|
+
throw NSError(domain: "Configuração inválida", code: 0)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
configuration.cardholderName = card.holder
|
|
141
|
+
configuration.primaryAccountSuffix = card.panTokenSuffix
|
|
142
|
+
configuration.paymentNetwork = PKPaymentNetwork.visa
|
|
143
|
+
|
|
144
|
+
let delegate = HandleDelegate(configuration: configuration, module: self)
|
|
145
|
+
self.activeDelegate = delegate
|
|
146
|
+
|
|
147
|
+
try await MainActor.run {
|
|
148
|
+
guard PKAddPaymentPassViewController.canAddPaymentPass() else {
|
|
149
|
+
throw NSError(domain: "PassKit indisponível", code: 0)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
guard let enrollViewController = delegate.createEnrollViewController() else {
|
|
153
|
+
throw NSError(domain: "Erro ao criar controlador", code: 0)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
guard let currentVC = appContext?.utilities?.currentViewController() else {
|
|
157
|
+
throw NSError(domain: "Sem controlador atual", code: 0)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
currentVC.present(enrollViewController, animated: true, completion: nil)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let result = try await delegate.waitForNonce()
|
|
164
|
+
|
|
165
|
+
return [
|
|
166
|
+
"nonce": result.nonce,
|
|
167
|
+
"nonceSignature": result.nonceSignature,
|
|
168
|
+
"certificates": result.certificates.joined(separator: ",")
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Function("completeEnrollment") {
|
|
173
|
+
(
|
|
174
|
+
activationDataBase64: String,
|
|
175
|
+
ephemeralPublicKeyBase64: String,
|
|
176
|
+
encryptedPassDataBase64: String
|
|
177
|
+
) in
|
|
178
|
+
|
|
179
|
+
guard let delegate = self.activeDelegate else {
|
|
180
|
+
print("Delegate não encontrado")
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
guard let activationData = Data(base64Encoded: activationDataBase64),
|
|
185
|
+
let ephemeralPublicKey = Data(base64Encoded: ephemeralPublicKeyBase64),
|
|
186
|
+
let encryptedPassData = Data(base64Encoded: encryptedPassDataBase64) else {
|
|
187
|
+
print("Dados inválidos")
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
delegate.continueEnrollment(
|
|
192
|
+
activationData: activationData,
|
|
193
|
+
ephemeralPublicKey: ephemeralPublicKey,
|
|
194
|
+
encryptedPassData: encryptedPassData
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
AsyncFunction("inAppVerification") { (serialNumber: String, passTypeIdentifier: String, action: String) in
|
|
199
|
+
print("inAppVerification: \(serialNumber) \(passTypeIdentifier) \(action)")
|
|
200
|
+
|
|
201
|
+
let passLibrary = PKPassLibrary()
|
|
202
|
+
|
|
203
|
+
var foundPass: PKSecureElementPass? = passLibrary.pass(withPassTypeIdentifier: passTypeIdentifier, serialNumber: serialNumber) as? PKSecureElementPass
|
|
204
|
+
|
|
205
|
+
if foundPass == nil {
|
|
206
|
+
let remotePasses = PKPassLibrary().remoteSecureElementPasses
|
|
207
|
+
foundPass = remotePasses.first(where: {
|
|
208
|
+
$0.passTypeIdentifier == passTypeIdentifier && $0.serialNumber == serialNumber
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
guard let securePass = foundPass else {
|
|
213
|
+
return [
|
|
214
|
+
"error": "CARD_NOT_FOUND",
|
|
215
|
+
"message": "Cartão não encontrado no dispositivo"
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
guard securePass.passActivationState == .requiresActivation else {
|
|
220
|
+
return [
|
|
221
|
+
"error": "INVALID_CARD_STATE",
|
|
222
|
+
"message": "Cartão em estado inválido para ativação"
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
var result: [String: String] = [
|
|
227
|
+
"deviceAccountNumberSuffix": securePass.deviceAccountNumberSuffix,
|
|
228
|
+
"primaryAccountIdentifier": securePass.primaryAccountIdentifier,
|
|
229
|
+
"activationState": "requiresActivation",
|
|
230
|
+
"serialNumber": securePass.serialNumber,
|
|
231
|
+
"passTypeIdentifier": securePass.passTypeIdentifier
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
let deviceName = UIDevice.current.name
|
|
235
|
+
result["deviceName"] = deviceName
|
|
236
|
+
|
|
237
|
+
return result
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhut-it/expo-apple-wallet",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A module to call apple wallet using Passkit API",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "expo-module build",
|
|
9
|
+
"clean": "expo-module clean",
|
|
10
|
+
"lint": "expo-module lint",
|
|
11
|
+
"test": "expo-module test",
|
|
12
|
+
"prepare": "expo-module prepare",
|
|
13
|
+
"prepublishOnly": "expo-module prepublishOnly",
|
|
14
|
+
"expo-module": "expo-module",
|
|
15
|
+
"open:ios": "xed example/ios",
|
|
16
|
+
"open:android": "open -a \"Android Studio\" example/android"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react-native",
|
|
20
|
+
"expo",
|
|
21
|
+
"expo-react-native-wallet",
|
|
22
|
+
"ExpoAppleWallet"
|
|
23
|
+
],
|
|
24
|
+
"author": "BHUT <contato@bhut.com.br>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "~18.3.12",
|
|
28
|
+
"expo-module-scripts": "^4.0.3",
|
|
29
|
+
"expo": "~52.0.0",
|
|
30
|
+
"react-native": "0.76.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"expo": "*",
|
|
34
|
+
"react": "*",
|
|
35
|
+
"react-native": "*"
|
|
36
|
+
},
|
|
37
|
+
"directories": {
|
|
38
|
+
"example": "example"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './ExpoAppleWalletModule';
|
package/tsconfig.json
ADDED