@capgo/capacitor-pay 7.0.0

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.
@@ -0,0 +1,454 @@
1
+ import Capacitor
2
+ import Contacts
3
+ import Foundation
4
+ import PassKit
5
+
6
+ @objc(PayPlugin)
7
+ public class PayPlugin: CAPPlugin, CAPBridgedPlugin, PKPaymentAuthorizationControllerDelegate {
8
+ public let identifier = "PayPlugin"
9
+ public let jsName = "Pay"
10
+ public let pluginMethods: [CAPPluginMethod] = [
11
+ CAPPluginMethod(name: "isPayAvailable", returnType: CAPPluginReturnPromise),
12
+ CAPPluginMethod(name: "requestPayment", returnType: CAPPluginReturnPromise)
13
+ ]
14
+
15
+ private var pendingApplePayCall: CAPPluginCall?
16
+ private var pendingApplePayment: PKPayment?
17
+ private var applePayController: PKPaymentAuthorizationController?
18
+
19
+ @objc func isPayAvailable(_ call: CAPPluginCall) {
20
+ let appleOptions = call.getObject("apple") ?? [:]
21
+ let requestedNetworks = networks(from: appleOptions["supportedNetworks"])
22
+
23
+ let canMakePayments = PKPaymentAuthorizationController.canMakePayments()
24
+ let canMakePaymentsUsingNetworks: Bool = requestedNetworks.isEmpty
25
+ ? canMakePayments
26
+ : PKPaymentAuthorizationController.canMakePayments(usingNetworks: requestedNetworks)
27
+
28
+ let available = canMakePayments && (requestedNetworks.isEmpty ? canMakePayments : canMakePaymentsUsingNetworks)
29
+
30
+ call.resolve([
31
+ "available": available,
32
+ "platform": "ios",
33
+ "apple": [
34
+ "canMakePayments": canMakePayments,
35
+ "canMakePaymentsUsingNetworks": canMakePaymentsUsingNetworks
36
+ ]
37
+ ])
38
+ }
39
+
40
+ @objc func requestPayment(_ call: CAPPluginCall) {
41
+ if pendingApplePayCall != nil {
42
+ call.reject("Another Apple Pay request is already in progress.")
43
+ return
44
+ }
45
+
46
+ guard let appleOptions = call.getObject("apple") else {
47
+ call.reject("Apple Pay configuration is required on iOS.")
48
+ return
49
+ }
50
+
51
+ do {
52
+ let request = try buildPaymentRequest(from: appleOptions)
53
+
54
+ guard PKPaymentAuthorizationController.canMakePayments() else {
55
+ throw PayPluginError.invalidConfiguration("Apple Pay is not available on this device.")
56
+ }
57
+
58
+ if !request.supportedNetworks.isEmpty &&
59
+ !PKPaymentAuthorizationController.canMakePayments(usingNetworks: request.supportedNetworks) {
60
+ throw PayPluginError.invalidConfiguration("None of the requested payment networks are available on this device.")
61
+ }
62
+
63
+ pendingApplePayCall = call
64
+ pendingApplePayment = nil
65
+
66
+ let controller = PKPaymentAuthorizationController(paymentRequest: request)
67
+ controller.delegate = self
68
+ applePayController = controller
69
+
70
+ DispatchQueue.main.async {
71
+ controller.present { presented in
72
+ if !presented {
73
+ self.rejectPendingCall("Failed to present Apple Pay sheet.")
74
+ }
75
+ }
76
+ }
77
+ } catch let error as PayPluginError {
78
+ call.reject(error.localizedDescription)
79
+ } catch {
80
+ call.reject("Failed to configure Apple Pay request.", nil, error)
81
+ }
82
+ }
83
+
84
+ // MARK: - PKPaymentAuthorizationControllerDelegate
85
+
86
+ public func paymentAuthorizationController(
87
+ _ controller: PKPaymentAuthorizationController,
88
+ didAuthorizePayment payment: PKPayment,
89
+ handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
90
+ ) {
91
+ pendingApplePayment = payment
92
+ completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
93
+ }
94
+
95
+ public func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) {
96
+ controller.dismiss {
97
+ guard let call = self.pendingApplePayCall else {
98
+ self.cleanupPendingTransaction()
99
+ return
100
+ }
101
+
102
+ defer { self.cleanupPendingTransaction() }
103
+
104
+ guard let payment = self.pendingApplePayment else {
105
+ call.reject("Payment canceled.")
106
+ return
107
+ }
108
+
109
+ do {
110
+ let result = try self.buildApplePayResult(from: payment)
111
+ call.resolve(result)
112
+ } catch {
113
+ call.reject("Failed to serialize Apple Pay result.", nil, error)
114
+ }
115
+ }
116
+ }
117
+
118
+ // MARK: - Helpers
119
+
120
+ private func buildPaymentRequest(from options: [String: Any]) throws -> PKPaymentRequest {
121
+ guard let merchantIdentifier = options["merchantIdentifier"] as? String, !merchantIdentifier.isEmpty else {
122
+ throw PayPluginError.invalidConfiguration("`merchantIdentifier` is required.")
123
+ }
124
+
125
+ guard let countryCode = options["countryCode"] as? String, !countryCode.isEmpty else {
126
+ throw PayPluginError.invalidConfiguration("`countryCode` is required.")
127
+ }
128
+
129
+ guard let currencyCode = options["currencyCode"] as? String, !currencyCode.isEmpty else {
130
+ throw PayPluginError.invalidConfiguration("`currencyCode` is required.")
131
+ }
132
+
133
+ let summaryItemsRaw = options["paymentSummaryItems"]
134
+ let summaryItems = paymentSummaryItems(from: summaryItemsRaw)
135
+ guard !summaryItems.isEmpty else {
136
+ throw PayPluginError.invalidConfiguration("`paymentSummaryItems` must include at least one item.")
137
+ }
138
+
139
+ let supportedNetworksRaw = options["supportedNetworks"]
140
+ let supportedNetworks = networks(from: supportedNetworksRaw)
141
+ guard !supportedNetworks.isEmpty else {
142
+ throw PayPluginError.invalidConfiguration("`supportedNetworks` must include at least one valid network.")
143
+ }
144
+
145
+ var merchantCapabilities: PKMerchantCapability = [.capability3DS]
146
+ if let capabilityValues = options["merchantCapabilities"] as? [String] {
147
+ let parsedCapabilities = parseMerchantCapabilities(from: capabilityValues)
148
+ if !parsedCapabilities.isEmpty {
149
+ merchantCapabilities = parsedCapabilities
150
+ }
151
+ }
152
+
153
+ var shippingContactFields: Set<PKContactField> = []
154
+ if let fields = options["requiredShippingContactFields"] as? [String] {
155
+ shippingContactFields = parseContactFields(from: fields)
156
+ }
157
+
158
+ var billingContactFields: Set<PKContactField> = []
159
+ if let fields = options["requiredBillingContactFields"] as? [String] {
160
+ billingContactFields = parseContactFields(from: fields)
161
+ }
162
+
163
+ let paymentRequest = PKPaymentRequest()
164
+ paymentRequest.merchantIdentifier = merchantIdentifier
165
+ paymentRequest.countryCode = countryCode
166
+ paymentRequest.currencyCode = currencyCode
167
+ paymentRequest.paymentSummaryItems = summaryItems
168
+ paymentRequest.supportedNetworks = supportedNetworks
169
+ paymentRequest.merchantCapabilities = merchantCapabilities
170
+
171
+ if !shippingContactFields.isEmpty {
172
+ paymentRequest.requiredShippingContactFields = shippingContactFields
173
+ }
174
+ if !billingContactFields.isEmpty {
175
+ paymentRequest.requiredBillingContactFields = billingContactFields
176
+ }
177
+
178
+ if let shippingTypeValue = options["shippingType"] as? String,
179
+ let shippingType = parseShippingType(from: shippingTypeValue) {
180
+ paymentRequest.shippingType = shippingType
181
+ }
182
+
183
+ if let supportedCountries = options["supportedCountries"] as? [String], !supportedCountries.isEmpty {
184
+ paymentRequest.supportedCountries = Set(supportedCountries)
185
+ }
186
+
187
+ if let applicationDataString = options["applicationData"] as? String {
188
+ if let data = Data(base64Encoded: applicationDataString) ?? applicationDataString.data(using: .utf8) {
189
+ paymentRequest.applicationData = data
190
+ }
191
+ }
192
+
193
+ return paymentRequest
194
+ }
195
+
196
+ private func paymentSummaryItems(from value: Any?) -> [PKPaymentSummaryItem] {
197
+ guard let items = value as? [Any] else {
198
+ return []
199
+ }
200
+
201
+ return items.compactMap { rawItem in
202
+ guard let item = rawItem as? [String: Any],
203
+ let label = item["label"] as? String,
204
+ let amountString = item["amount"] as? String else {
205
+ return nil
206
+ }
207
+
208
+ let amount = NSDecimalNumber(string: amountString)
209
+ if amount == NSDecimalNumber.notANumber {
210
+ return nil
211
+ }
212
+ let summaryItem = PKPaymentSummaryItem(label: label, amount: amount)
213
+
214
+ if let typeString = item["type"] as? String {
215
+ switch typeString.lowercased() {
216
+ case "pending":
217
+ summaryItem.type = .pending
218
+ default:
219
+ summaryItem.type = .final
220
+ }
221
+ }
222
+
223
+ return summaryItem
224
+ }
225
+ }
226
+
227
+ private func networks(from value: Any?) -> [PKPaymentNetwork] {
228
+ guard let networkStrings = value as? [Any] else {
229
+ return []
230
+ }
231
+
232
+ return networkStrings.compactMap { element in
233
+ if let stringValue = element as? String {
234
+ return PKPaymentNetwork(rawValue: stringValue)
235
+ }
236
+ return nil
237
+ }
238
+ }
239
+
240
+ private func parseMerchantCapabilities(from values: [String]) -> PKMerchantCapability {
241
+ var capabilities: PKMerchantCapability = []
242
+
243
+ for value in values {
244
+ switch value.lowercased() {
245
+ case "3ds":
246
+ capabilities.insert(.capability3DS)
247
+ case "credit":
248
+ capabilities.insert(.capabilityCredit)
249
+ case "debit":
250
+ capabilities.insert(.capabilityDebit)
251
+ case "emv":
252
+ capabilities.insert(.capabilityEMV)
253
+ default:
254
+ continue
255
+ }
256
+ }
257
+
258
+ return capabilities
259
+ }
260
+
261
+ private func parseContactFields(from values: [String]) -> Set<PKContactField> {
262
+ var fields = Set<PKContactField>()
263
+
264
+ for value in values {
265
+ switch value {
266
+ case "emailAddress":
267
+ fields.insert(.emailAddress)
268
+ case "name":
269
+ fields.insert(.name)
270
+ case "phoneNumber":
271
+ fields.insert(.phoneNumber)
272
+ case "postalAddress":
273
+ fields.insert(.postalAddress)
274
+ default:
275
+ continue
276
+ }
277
+ }
278
+
279
+ return fields
280
+ }
281
+
282
+ private func parseShippingType(from value: String) -> PKShippingType? {
283
+ switch value {
284
+ case "shipping":
285
+ return .shipping
286
+ case "delivery":
287
+ return .delivery
288
+ case "servicePickup":
289
+ return .servicePickup
290
+ case "storePickup":
291
+ return .storePickup
292
+ default:
293
+ return nil
294
+ }
295
+ }
296
+
297
+ private func buildApplePayResult(from payment: PKPayment) throws -> [String: Any] {
298
+ let paymentData = payment.token.paymentData
299
+ let paymentDataBase64 = paymentData.base64EncodedString()
300
+ let paymentString = String(data: paymentData, encoding: .utf8) ?? paymentDataBase64
301
+
302
+ var paymentMethod: [String: Any] = [
303
+ "type": mapPaymentMethodType(payment.token.paymentMethod.type)
304
+ ]
305
+
306
+ if let displayName = payment.token.paymentMethod.displayName {
307
+ paymentMethod["displayName"] = displayName
308
+ }
309
+
310
+ if let network = payment.token.paymentMethod.network {
311
+ paymentMethod["network"] = network.rawValue
312
+ }
313
+
314
+ var appleResult: [String: Any] = [
315
+ "paymentData": paymentDataBase64,
316
+ "paymentString": paymentString,
317
+ "transactionIdentifier": payment.token.transactionIdentifier,
318
+ "paymentMethod": paymentMethod
319
+ ]
320
+
321
+ if let shippingContact = contactDictionary(from: payment.shippingContact) {
322
+ appleResult["shippingContact"] = shippingContact
323
+ }
324
+
325
+ if let billingContact = contactDictionary(from: payment.billingContact) {
326
+ appleResult["billingContact"] = billingContact
327
+ }
328
+
329
+ return [
330
+ "platform": "ios",
331
+ "apple": appleResult
332
+ ]
333
+ }
334
+
335
+ private func contactDictionary(from contact: PKContact?) -> [String: Any]? {
336
+ guard let contact else {
337
+ return nil
338
+ }
339
+
340
+ var result: [String: Any] = [:]
341
+
342
+ if let nameComponents = contact.name {
343
+ var name: [String: String] = [:]
344
+ if let givenName = nameComponents.givenName {
345
+ name["givenName"] = givenName
346
+ }
347
+ if let familyName = nameComponents.familyName {
348
+ name["familyName"] = familyName
349
+ }
350
+ if let middleName = nameComponents.middleName {
351
+ name["middleName"] = middleName
352
+ }
353
+ if let namePrefix = nameComponents.namePrefix {
354
+ name["namePrefix"] = namePrefix
355
+ }
356
+ if let nameSuffix = nameComponents.nameSuffix {
357
+ name["nameSuffix"] = nameSuffix
358
+ }
359
+ if let nickname = nameComponents.nickname {
360
+ name["nickname"] = nickname
361
+ }
362
+ if !name.isEmpty {
363
+ result["name"] = name
364
+ }
365
+ }
366
+
367
+ if let email = contact.emailAddress {
368
+ result["emailAddress"] = email
369
+ }
370
+
371
+ if let phone = contact.phoneNumber?.stringValue {
372
+ result["phoneNumber"] = phone
373
+ }
374
+
375
+ if let postal = contact.postalAddress {
376
+ var address: [String: String] = [:]
377
+ if !postal.street.isEmpty {
378
+ address["street"] = postal.street
379
+ }
380
+ if !postal.city.isEmpty {
381
+ address["city"] = postal.city
382
+ }
383
+ if !postal.state.isEmpty {
384
+ address["state"] = postal.state
385
+ }
386
+ if !postal.postalCode.isEmpty {
387
+ address["postalCode"] = postal.postalCode
388
+ }
389
+ if !postal.country.isEmpty {
390
+ address["country"] = postal.country
391
+ }
392
+ if !postal.isoCountryCode.isEmpty {
393
+ address["isoCountryCode"] = postal.isoCountryCode
394
+ }
395
+ if !postal.subAdministrativeArea.isEmpty {
396
+ address["subAdministrativeArea"] = postal.subAdministrativeArea
397
+ }
398
+ if !postal.subLocality.isEmpty {
399
+ address["subLocality"] = postal.subLocality
400
+ }
401
+ if !address.isEmpty {
402
+ result["postalAddress"] = address
403
+ }
404
+ }
405
+
406
+ return result.isEmpty ? nil : result
407
+ }
408
+
409
+ private func mapPaymentMethodType(_ type: PKPaymentMethodType) -> String {
410
+ switch type {
411
+ case .debit:
412
+ return "debit"
413
+ case .credit:
414
+ return "credit"
415
+ case .prepaid:
416
+ return "prepaid"
417
+ case .store:
418
+ return "store"
419
+ default:
420
+ return "unknown"
421
+ }
422
+ }
423
+
424
+ private func rejectPendingCall(_ message: String, error: Error? = nil) {
425
+ guard let call = pendingApplePayCall else {
426
+ return
427
+ }
428
+
429
+ if let error {
430
+ call.reject(message, nil, error)
431
+ } else {
432
+ call.reject(message)
433
+ }
434
+ cleanupPendingTransaction()
435
+ }
436
+
437
+ private func cleanupPendingTransaction() {
438
+ pendingApplePayCall = nil
439
+ pendingApplePayment = nil
440
+ applePayController?.delegate = nil
441
+ applePayController = nil
442
+ }
443
+ }
444
+
445
+ private enum PayPluginError: LocalizedError {
446
+ case invalidConfiguration(String)
447
+
448
+ var errorDescription: String? {
449
+ switch self {
450
+ case let .invalidConfiguration(message):
451
+ return message
452
+ }
453
+ }
454
+ }
@@ -0,0 +1,15 @@
1
+ import XCTest
2
+ @testable import PayPlugin
3
+
4
+ class PayTests: XCTestCase {
5
+ func testEcho() {
6
+ // This is an example of a functional test case for a plugin.
7
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
8
+
9
+ let implementation = Pay()
10
+ let value = "Hello, World!"
11
+ let result = implementation.echo(value)
12
+
13
+ XCTAssertEqual(value, result)
14
+ }
15
+ }
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@capgo/capacitor-pay",
3
+ "version": "7.0.0",
4
+ "description": "Capacitor plugin to trigger native payment for iOS(Apple pay) and Android(Google Pay)",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "docs/",
16
+ "Package.swift",
17
+ "CapgoCapacitorPay.podspec"
18
+ ],
19
+ "author": "Martin Donadieu <martin@capgo.app>",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/Cap-go/capacitor-pay.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/Cap-go/capacitor-pay/issues"
27
+ },
28
+ "keywords": [
29
+ "capacitor",
30
+ "plugin",
31
+ "native",
32
+ "Apple pay",
33
+ "Google pay",
34
+ "Native payment"
35
+ ],
36
+ "scripts": {
37
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
38
+ "verify:ios": "xcodebuild -scheme CapgoCapacitorPay -destination generic/platform=iOS",
39
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
40
+ "verify:web": "npm run build",
41
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
42
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
43
+ "eslint": "eslint . --ext ts",
44
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
45
+ "swiftlint": "node-swiftlint",
46
+ "docgen": "docgen --api PayPlugin --output-readme README.md --output-json dist/docs.json",
47
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
48
+ "clean": "rimraf ./dist",
49
+ "watch": "tsc --watch",
50
+ "prepublishOnly": "npm run build"
51
+ },
52
+ "devDependencies": {
53
+ "@capacitor/android": "^7.0.0",
54
+ "@capacitor/core": "^7.0.0",
55
+ "@capacitor/docgen": "^0.3.0",
56
+ "@capacitor/ios": "^7.0.0",
57
+ "@ionic/eslint-config": "^0.4.0",
58
+ "@ionic/prettier-config": "^4.0.0",
59
+ "@ionic/swiftlint-config": "^2.0.0",
60
+ "eslint": "^8.57.0",
61
+ "prettier": "^3.4.2",
62
+ "prettier-plugin-java": "^2.6.6",
63
+ "rimraf": "^6.0.1",
64
+ "rollup": "^4.30.1",
65
+ "swiftlint": "^2.0.0",
66
+ "typescript": "~4.1.5"
67
+ },
68
+ "peerDependencies": {
69
+ "@capacitor/core": ">=7.0.0"
70
+ },
71
+ "prettier": "@ionic/prettier-config",
72
+ "swiftlint": "@ionic/swiftlint-config",
73
+ "eslintConfig": {
74
+ "extends": "@ionic/eslint-config/recommended"
75
+ },
76
+ "capacitor": {
77
+ "ios": {
78
+ "src": "ios"
79
+ },
80
+ "android": {
81
+ "src": "android"
82
+ }
83
+ }
84
+ }