expo-iap 2.7.13 → 2.8.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.
- package/.copilot-instructions.md +5 -5
- package/.cursorrules +31 -13
- package/CHANGELOG.md +16 -1
- package/CLAUDE.md +5 -1
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +2 -0
- package/build/ExpoIap.types.d.ts +24 -11
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js.map +1 -1
- package/build/helpers/subscription.d.ts.map +1 -1
- package/build/helpers/subscription.js +24 -17
- package/build/helpers/subscription.js.map +1 -1
- package/build/index.d.ts +2 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +12 -17
- package/build/index.js.map +1 -1
- package/build/modules/ios.d.ts +10 -23
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +9 -23
- package/build/modules/ios.js.map +1 -1
- package/build/types/ExpoIapAndroid.types.d.ts +6 -0
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
- package/build/types/ExpoIapAndroid.types.js.map +1 -1
- package/build/types/{ExpoIapIos.types.d.ts → ExpoIapIOS.types.d.ts} +45 -33
- package/build/types/{ExpoIapIos.types.d.ts.map → ExpoIapIOS.types.d.ts.map} +1 -1
- package/build/types/ExpoIapIOS.types.js +2 -0
- package/build/types/ExpoIapIOS.types.js.map +1 -0
- package/build/{useIap.d.ts → useIAP.d.ts} +2 -2
- package/build/{useIap.d.ts.map → useIAP.d.ts.map} +1 -1
- package/build/{useIap.js → useIAP.js} +7 -6
- package/build/useIAP.js.map +1 -0
- package/bun.lock +677 -61
- package/ios/ExpoIapModule.swift +46 -45
- package/jest.config.js +43 -0
- package/package.json +8 -3
- package/src/ExpoIap.types.ts +34 -15
- package/src/helpers/subscription.ts +27 -20
- package/src/index.ts +16 -24
- package/src/modules/ios.ts +13 -40
- package/src/types/ExpoIapAndroid.types.ts +6 -0
- package/src/types/{ExpoIapIos.types.ts → ExpoIapIOS.types.ts} +46 -32
- package/src/{useIap.ts → useIAP.ts} +7 -6
- package/build/types/ExpoIapIos.types.js +0 -2
- package/build/types/ExpoIapIos.types.js.map +0 -1
- package/build/useIap.js.map +0 -1
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -22,12 +22,12 @@ struct IapEvent {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
@available(iOS 15.0, *)
|
|
25
|
-
func serializeTransaction(_ transaction: Transaction,
|
|
25
|
+
func serializeTransaction(_ transaction: Transaction, jwsRepresentationIOS: String? = nil) -> [String: Any?] {
|
|
26
26
|
let _ =
|
|
27
27
|
transaction.productType.rawValue.lowercased().contains("renewable")
|
|
28
28
|
|| transaction.expirationDate != nil
|
|
29
29
|
|
|
30
|
-
var
|
|
30
|
+
var transactionReasonIOS: String? = nil
|
|
31
31
|
var webOrderLineItemId: Int? = nil
|
|
32
32
|
var jsonData: [String: Any]? = nil
|
|
33
33
|
var jwsReceipt: String = ""
|
|
@@ -38,7 +38,7 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
38
38
|
do {
|
|
39
39
|
if let jsonObj = try JSONSerialization.jsonObject(with: jsonRep) as? [String: Any] {
|
|
40
40
|
jsonData = jsonObj
|
|
41
|
-
|
|
41
|
+
transactionReasonIOS = jsonObj["transactionReason"] as? String
|
|
42
42
|
if let webOrderId = jsonObj["webOrderLineItemID"] as? NSNumber {
|
|
43
43
|
webOrderLineItemId = webOrderId.intValue
|
|
44
44
|
}
|
|
@@ -56,46 +56,47 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
56
56
|
"transactionReceipt": jwsReceipt,
|
|
57
57
|
"platform": "ios",
|
|
58
58
|
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
59
|
+
"quantityIOS": transaction.purchasedQuantity,
|
|
60
|
+
"originalTransactionDateIOS": transaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
61
|
+
"originalTransactionIdentifierIOS": String(transaction.originalID),
|
|
62
62
|
"appAccountToken": transaction.appAccountToken?.uuidString,
|
|
63
63
|
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
64
|
+
"appBundleIdIOS": transaction.appBundleID,
|
|
65
|
+
"productTypeIOS": transaction.productType.rawValue,
|
|
66
|
+
"subscriptionGroupIdIOS": transaction.subscriptionGroupID,
|
|
67
67
|
|
|
68
|
-
"
|
|
68
|
+
"webOrderLineItemIdIOS": webOrderLineItemId,
|
|
69
69
|
|
|
70
|
-
"
|
|
70
|
+
"expirationDateIOS": transaction.expirationDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
71
71
|
|
|
72
|
-
"
|
|
73
|
-
"
|
|
72
|
+
"isUpgradedIOS": transaction.isUpgraded,
|
|
73
|
+
"ownershipTypeIOS": transaction.ownershipType.rawValue,
|
|
74
74
|
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
75
|
+
"revocationDateIOS": transaction.revocationDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
76
|
+
"revocationReasonIOS": transaction.revocationReason?.rawValue,
|
|
77
|
+
"transactionReasonIOS": transactionReasonIOS,
|
|
78
78
|
]
|
|
79
79
|
|
|
80
|
-
if (
|
|
81
|
-
logDebug("serializeTransaction adding
|
|
82
|
-
purchaseMap["
|
|
80
|
+
if (jwsRepresentationIOS != nil) {
|
|
81
|
+
logDebug("serializeTransaction adding jwsRepresentationIOS with length: \(jwsRepresentationIOS!.count)")
|
|
82
|
+
purchaseMap["jwsRepresentationIOS"] = jwsRepresentationIOS
|
|
83
|
+
purchaseMap["purchaseToken"] = jwsRepresentationIOS
|
|
83
84
|
} else {
|
|
84
|
-
logDebug("serializeTransaction
|
|
85
|
+
logDebug("serializeTransaction jwsRepresentationIOS is nil")
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
if #available(iOS 16.0, *) {
|
|
88
|
-
purchaseMap["
|
|
89
|
+
purchaseMap["environmentIOS"] = transaction.environment.rawValue
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
if #available(iOS 17.0, *) {
|
|
92
|
-
purchaseMap["
|
|
93
|
-
purchaseMap["
|
|
93
|
+
purchaseMap["storefrontCountryCodeIOS"] = transaction.storefront.countryCode
|
|
94
|
+
purchaseMap["reasonIOS"] = transaction.reason.rawValue
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
if #available(iOS 17.2, *) {
|
|
97
98
|
if let offer = transaction.offer {
|
|
98
|
-
purchaseMap["
|
|
99
|
+
purchaseMap["offerIOS"] = [
|
|
99
100
|
"id": offer.id ?? "",
|
|
100
101
|
"type": offer.type.rawValue,
|
|
101
102
|
"paymentMode": offer.paymentMode?.rawValue ?? "",
|
|
@@ -105,10 +106,10 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
105
106
|
|
|
106
107
|
if #available(iOS 15.4, *), let jsonData = jsonData {
|
|
107
108
|
if let price = jsonData["price"] as? NSNumber {
|
|
108
|
-
purchaseMap["
|
|
109
|
+
purchaseMap["priceIOS"] = price.doubleValue
|
|
109
110
|
}
|
|
110
111
|
if let currency = jsonData["currency"] as? String {
|
|
111
|
-
purchaseMap["
|
|
112
|
+
purchaseMap["currencyIOS"] = currency
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -117,7 +118,7 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
117
118
|
|
|
118
119
|
private let DEFAULT_SUBSCRIPTION_PERIOD_UNIT = "DAY" // Default fallback unit for subscription periods.
|
|
119
120
|
|
|
120
|
-
func
|
|
121
|
+
func getPeriodIOS(_ unit: Product.SubscriptionPeriod.Unit) -> String {
|
|
121
122
|
return switch (unit) {
|
|
122
123
|
case .day: "DAY"
|
|
123
124
|
case .week: "WEEK"
|
|
@@ -134,7 +135,7 @@ func serializeOffer(_ offer: Product.SubscriptionOffer?) -> [String: Any?]? {
|
|
|
134
135
|
return [
|
|
135
136
|
"id": offer.id,
|
|
136
137
|
"period": [
|
|
137
|
-
"unit":
|
|
138
|
+
"unit": getPeriodIOS(offer.period.unit),
|
|
138
139
|
"value": offer.period.value
|
|
139
140
|
],
|
|
140
141
|
"periodCount": offer.periodCount,
|
|
@@ -150,9 +151,9 @@ func serializeSubscription(_ s: Product.SubscriptionInfo?) -> [String: Any?]? {
|
|
|
150
151
|
return [
|
|
151
152
|
"introductoryOffer": serializeOffer(s.introductoryOffer),
|
|
152
153
|
"promotionalOffers": s.promotionalOffers.map(serializeOffer),
|
|
153
|
-
"
|
|
154
|
+
"subscriptionGroupId": s.subscriptionGroupID,
|
|
154
155
|
"subscriptionPeriod": [
|
|
155
|
-
"unit":
|
|
156
|
+
"unit": getPeriodIOS(s.subscriptionPeriod.unit),
|
|
156
157
|
"value": s.subscriptionPeriod.value
|
|
157
158
|
],
|
|
158
159
|
]
|
|
@@ -289,7 +290,7 @@ public class ExpoIapModule: Module {
|
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
var result: [String: Any?] = [
|
|
292
|
-
"
|
|
293
|
+
"bundleId": appTransaction.bundleID,
|
|
293
294
|
"appVersion": appTransaction.appVersion,
|
|
294
295
|
"originalAppVersion": appTransaction.originalAppVersion,
|
|
295
296
|
"originalPurchaseDate": appTransaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
@@ -297,13 +298,13 @@ public class ExpoIapModule: Module {
|
|
|
297
298
|
"deviceVerificationNonce": appTransaction.deviceVerificationNonce.uuidString,
|
|
298
299
|
"environment": appTransaction.environment.rawValue,
|
|
299
300
|
"signedDate": appTransaction.signedDate.timeIntervalSince1970 * 1000,
|
|
300
|
-
"
|
|
301
|
-
"
|
|
301
|
+
"appId": appTransaction.appID,
|
|
302
|
+
"appVersionId": appTransaction.appVersionID,
|
|
302
303
|
"preorderDate": appTransaction.preorderDate.map { $0.timeIntervalSince1970 * 1000 }
|
|
303
304
|
]
|
|
304
305
|
|
|
305
306
|
if #available(iOS 18.4, *) {
|
|
306
|
-
result["
|
|
307
|
+
result["appTransactionId"] = appTransaction.appTransactionID
|
|
307
308
|
result["originalPlatform"] = appTransaction.originalPlatform.rawValue
|
|
308
309
|
}
|
|
309
310
|
|
|
@@ -392,8 +393,8 @@ public class ExpoIapModule: Module {
|
|
|
392
393
|
|
|
393
394
|
var purchasedItemsSerialized: [[String: Any?]] = []
|
|
394
395
|
|
|
395
|
-
func addTransaction(transaction: Transaction,
|
|
396
|
-
let serialized = serializeTransaction(transaction,
|
|
396
|
+
func addTransaction(transaction: Transaction, jwsRepresentationIOS: String? = nil) {
|
|
397
|
+
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: jwsRepresentationIOS)
|
|
397
398
|
purchasedItemsSerialized.append(serialized)
|
|
398
399
|
|
|
399
400
|
if alsoPublishToEventListener {
|
|
@@ -407,7 +408,7 @@ public class ExpoIapModule: Module {
|
|
|
407
408
|
do {
|
|
408
409
|
let transaction = try self.checkVerified(verification)
|
|
409
410
|
if !onlyIncludeActiveItems {
|
|
410
|
-
addTransaction(transaction: transaction,
|
|
411
|
+
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
411
412
|
continue
|
|
412
413
|
}
|
|
413
414
|
switch transaction.productType {
|
|
@@ -415,7 +416,7 @@ public class ExpoIapModule: Module {
|
|
|
415
416
|
if await self.productStore?.getProduct(productID: transaction.productID)
|
|
416
417
|
!= nil
|
|
417
418
|
{
|
|
418
|
-
addTransaction(transaction: transaction,
|
|
419
|
+
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
419
420
|
}
|
|
420
421
|
case .nonRenewable:
|
|
421
422
|
if await self.productStore?.getProduct(productID: transaction.productID)
|
|
@@ -425,7 +426,7 @@ public class ExpoIapModule: Module {
|
|
|
425
426
|
let expirationDate = Calendar(identifier: .gregorian).date(
|
|
426
427
|
byAdding: DateComponents(year: 1), to: transaction.purchaseDate)!
|
|
427
428
|
if currentDate < expirationDate {
|
|
428
|
-
addTransaction(transaction: transaction,
|
|
429
|
+
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
429
430
|
}
|
|
430
431
|
}
|
|
431
432
|
default:
|
|
@@ -536,10 +537,10 @@ public class ExpoIapModule: Module {
|
|
|
536
537
|
return nil
|
|
537
538
|
} else {
|
|
538
539
|
self.transactions[String(transaction.id)] = transaction
|
|
539
|
-
let serialized = serializeTransaction(transaction,
|
|
540
|
+
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
540
541
|
|
|
541
|
-
// Debug: Check if
|
|
542
|
-
logDebug("buyProduct serialized includes JWS: \(serialized["
|
|
542
|
+
// Debug: Check if jwsRepresentationIOS is included in serialized result
|
|
543
|
+
logDebug("buyProduct serialized includes JWS: \(serialized["jwsRepresentationIOS"] != nil)")
|
|
543
544
|
|
|
544
545
|
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
545
546
|
return serialized
|
|
@@ -887,7 +888,7 @@ public class ExpoIapModule: Module {
|
|
|
887
888
|
// If this doesn't throw, the transaction is verified
|
|
888
889
|
let transaction = try self.checkVerified(result)
|
|
889
890
|
isValid = true
|
|
890
|
-
latestTransaction = serializeTransaction(transaction,
|
|
891
|
+
latestTransaction = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
891
892
|
} catch {
|
|
892
893
|
isValid = false
|
|
893
894
|
}
|
|
@@ -973,7 +974,7 @@ public class ExpoIapModule: Module {
|
|
|
973
974
|
let transaction = try self.checkVerified(result)
|
|
974
975
|
self.transactions[String(transaction.id)] = transaction
|
|
975
976
|
if self.hasListeners {
|
|
976
|
-
let serialized = serializeTransaction(transaction,
|
|
977
|
+
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
977
978
|
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
978
979
|
}
|
|
979
980
|
} catch {
|
|
@@ -1075,7 +1076,7 @@ public class ExpoIapModule: Module {
|
|
|
1075
1076
|
previousWillAutoRenew != currentWillAutoRenew {
|
|
1076
1077
|
|
|
1077
1078
|
// Use the jwsRepresentation when serializing the transaction
|
|
1078
|
-
var purchaseMap = serializeTransaction(transaction,
|
|
1079
|
+
var purchaseMap = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
1079
1080
|
|
|
1080
1081
|
if case .verified(let renewalInfo) = status.renewalInfo {
|
|
1081
1082
|
if let renewalInfoDict = serializeRenewalInfo(.verified(renewalInfo)) {
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: 'ts-jest',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
roots: ['<rootDir>/src'],
|
|
5
|
+
testMatch: [
|
|
6
|
+
'**/__tests__/**/*.+(ts|tsx|js)',
|
|
7
|
+
'**/?(*.)+(spec|test).+(ts|tsx|js)',
|
|
8
|
+
],
|
|
9
|
+
transform: {
|
|
10
|
+
'^.+\\.(ts|tsx)$': [
|
|
11
|
+
'ts-jest',
|
|
12
|
+
{
|
|
13
|
+
tsconfig: {
|
|
14
|
+
jsx: 'react',
|
|
15
|
+
esModuleInterop: true,
|
|
16
|
+
allowSyntheticDefaultImports: true,
|
|
17
|
+
moduleResolution: 'node',
|
|
18
|
+
skipLibCheck: true,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
moduleNameMapper: {
|
|
24
|
+
'^react-native$': '<rootDir>/src/__mocks__/react-native.js',
|
|
25
|
+
'^expo-modules-core$': '<rootDir>/src/__mocks__/expo-modules-core.js',
|
|
26
|
+
},
|
|
27
|
+
collectCoverageFrom: [
|
|
28
|
+
'src/**/*.{ts,tsx}',
|
|
29
|
+
'!src/**/*.d.ts',
|
|
30
|
+
'!src/**/__tests__/**',
|
|
31
|
+
'!src/**/__mocks__/**',
|
|
32
|
+
'!src/ExpoIapModule.ts',
|
|
33
|
+
'!src/ExpoIapModule.web.ts',
|
|
34
|
+
],
|
|
35
|
+
coverageThreshold: {
|
|
36
|
+
global: {
|
|
37
|
+
branches: 15,
|
|
38
|
+
functions: 15,
|
|
39
|
+
lines: 15,
|
|
40
|
+
statements: 15,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "In App Purchase module in Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"lint:prettier": "prettier --write \"**/*.{md,js,jsx,ts,tsx}\"",
|
|
13
13
|
"lint:tsc": "tsc -p tsconfig.json --noEmit --skipLibCheck",
|
|
14
14
|
"lint:ci": "bun run lint:tsc && bun run lint:eslint && bun run lint:prettier",
|
|
15
|
+
"test": "jest",
|
|
16
|
+
"test:coverage": "jest --coverage",
|
|
15
17
|
"prepare": "expo-module prepare",
|
|
16
18
|
"expo-module": "expo-module",
|
|
17
19
|
"open:ios": "xed example/ios",
|
|
@@ -38,15 +40,18 @@
|
|
|
38
40
|
"author": "hyochan <hyochan.dev@gmail.com> (https://github.com/hyochan)",
|
|
39
41
|
"license": "MIT",
|
|
40
42
|
"homepage": "https://github.com/hyochan/expo-iap#readme",
|
|
41
|
-
"dependencies": {},
|
|
42
43
|
"devDependencies": {
|
|
44
|
+
"@jest/globals": "^30.0.5",
|
|
45
|
+
"@types/jest": "^30.0.0",
|
|
43
46
|
"@types/react": "~19.1.7",
|
|
44
47
|
"eslint": "8.57.0",
|
|
45
48
|
"eslint-config-expo": "^9.2.0",
|
|
46
49
|
"eslint-config-prettier": "^10.1.5",
|
|
47
50
|
"eslint-plugin-prettier": "^5.4.1",
|
|
48
51
|
"expo-module-scripts": "^4.1.7",
|
|
49
|
-
"expo-modules-core": "^2.4.0"
|
|
52
|
+
"expo-modules-core": "^2.4.0",
|
|
53
|
+
"jest": "^29.7.0",
|
|
54
|
+
"ts-jest": "^29.4.1"
|
|
50
55
|
},
|
|
51
56
|
"peerDependencies": {
|
|
52
57
|
"expo": "*",
|
package/src/ExpoIap.types.ts
CHANGED
|
@@ -4,10 +4,10 @@ import {
|
|
|
4
4
|
SubscriptionProductAndroid,
|
|
5
5
|
} from './types/ExpoIapAndroid.types';
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from './types/
|
|
7
|
+
ProductIOS,
|
|
8
|
+
ProductPurchaseIOS,
|
|
9
|
+
SubscriptionProductIOS,
|
|
10
|
+
} from './types/ExpoIapIOS.types';
|
|
11
11
|
import {NATIVE_ERROR_CODES} from './ExpoIapModule';
|
|
12
12
|
|
|
13
13
|
export type ChangeEventPayload = {
|
|
@@ -33,6 +33,7 @@ export type PurchaseBase = {
|
|
|
33
33
|
transactionId?: string; // @deprecated - use id instead
|
|
34
34
|
transactionDate: number;
|
|
35
35
|
transactionReceipt: string;
|
|
36
|
+
purchaseToken?: string; // Unified purchase token (jwsRepresentation for iOS, purchaseToken for Android)
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
// Define literal platform types for better type discrimination
|
|
@@ -42,29 +43,34 @@ export type AndroidPlatform = {platform: 'android'};
|
|
|
42
43
|
// Platform-agnostic unified product types (public API)
|
|
43
44
|
export type Product =
|
|
44
45
|
| (ProductAndroid & AndroidPlatform)
|
|
45
|
-
| (
|
|
46
|
+
| (ProductIOS & IosPlatform);
|
|
46
47
|
|
|
47
48
|
export type SubscriptionProduct =
|
|
48
49
|
| (SubscriptionProductAndroid & AndroidPlatform)
|
|
49
|
-
| (
|
|
50
|
+
| (SubscriptionProductIOS & IosPlatform);
|
|
50
51
|
|
|
51
52
|
// ============================================================================
|
|
52
|
-
// Legacy Types (For backward compatibility with
|
|
53
|
+
// Legacy Types (For backward compatibility with useIAP hook)
|
|
53
54
|
// ============================================================================
|
|
54
55
|
|
|
55
56
|
// Re-export platform-specific purchase types for legacy compatibility
|
|
56
57
|
export type {ProductPurchaseAndroid} from './types/ExpoIapAndroid.types';
|
|
57
|
-
export type {
|
|
58
|
+
export type {ProductPurchaseIOS} from './types/ExpoIapIOS.types';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @deprecated Use ProductPurchaseIOS instead. This alias will be removed in v3.0.0.
|
|
62
|
+
*/
|
|
63
|
+
export type ProductPurchaseIos = ProductPurchaseIOS;
|
|
58
64
|
|
|
59
65
|
// Union type for platform-specific purchase types (legacy support)
|
|
60
66
|
export type ProductPurchase =
|
|
61
67
|
| (ProductPurchaseAndroid & AndroidPlatform)
|
|
62
|
-
| (
|
|
68
|
+
| (ProductPurchaseIOS & IosPlatform);
|
|
63
69
|
|
|
64
70
|
// Union type for platform-specific subscription purchase types (legacy support)
|
|
65
71
|
export type SubscriptionPurchase =
|
|
66
72
|
| (ProductPurchaseAndroid & AndroidPlatform & {autoRenewingAndroid: boolean})
|
|
67
|
-
| (
|
|
73
|
+
| (ProductPurchaseIOS & IosPlatform);
|
|
68
74
|
|
|
69
75
|
export type Purchase = ProductPurchase | SubscriptionPurchase;
|
|
70
76
|
|
|
@@ -74,7 +80,11 @@ export type PurchaseResult = {
|
|
|
74
80
|
debugMessage?: string;
|
|
75
81
|
code?: string;
|
|
76
82
|
message?: string;
|
|
83
|
+
/**
|
|
84
|
+
* @deprecated Use `purchaseToken` instead. This field will be removed in a future version.
|
|
85
|
+
*/
|
|
77
86
|
purchaseTokenAndroid?: string;
|
|
87
|
+
purchaseToken?: string;
|
|
78
88
|
};
|
|
79
89
|
/**
|
|
80
90
|
* Centralized error codes for expo-iap
|
|
@@ -303,7 +313,7 @@ export interface UnifiedRequestPurchaseProps {
|
|
|
303
313
|
readonly andDangerouslyFinishTransactionAutomaticallyIOS?: boolean;
|
|
304
314
|
readonly appAccountToken?: string;
|
|
305
315
|
readonly quantity?: number;
|
|
306
|
-
readonly withOffer?: import('./types/
|
|
316
|
+
readonly withOffer?: import('./types/ExpoIapIOS.types').PaymentDiscount;
|
|
307
317
|
|
|
308
318
|
// Android-specific properties (ignored on iOS)
|
|
309
319
|
readonly obfuscatedAccountIdAndroid?: string;
|
|
@@ -318,12 +328,12 @@ export interface UnifiedRequestPurchaseProps {
|
|
|
318
328
|
/**
|
|
319
329
|
* iOS-specific purchase request parameters
|
|
320
330
|
*/
|
|
321
|
-
export interface
|
|
331
|
+
export interface RequestPurchaseIOSProps {
|
|
322
332
|
readonly sku: string;
|
|
323
333
|
readonly andDangerouslyFinishTransactionAutomaticallyIOS?: boolean;
|
|
324
334
|
readonly appAccountToken?: string;
|
|
325
335
|
readonly quantity?: number;
|
|
326
|
-
readonly withOffer?: import('./types/
|
|
336
|
+
readonly withOffer?: import('./types/ExpoIapIOS.types').PaymentDiscount;
|
|
327
337
|
}
|
|
328
338
|
|
|
329
339
|
/**
|
|
@@ -354,7 +364,7 @@ export interface RequestSubscriptionAndroidProps
|
|
|
354
364
|
* Allows clear separation of iOS and Android parameters
|
|
355
365
|
*/
|
|
356
366
|
export interface RequestPurchasePropsByPlatforms {
|
|
357
|
-
readonly ios?:
|
|
367
|
+
readonly ios?: RequestPurchaseIOSProps;
|
|
358
368
|
readonly android?: RequestPurchaseAndroidProps;
|
|
359
369
|
}
|
|
360
370
|
|
|
@@ -362,7 +372,7 @@ export interface RequestPurchasePropsByPlatforms {
|
|
|
362
372
|
* Modern platform-specific subscription request structure (v2.7.0+)
|
|
363
373
|
*/
|
|
364
374
|
export interface RequestSubscriptionPropsByPlatforms {
|
|
365
|
-
readonly ios?:
|
|
375
|
+
readonly ios?: RequestPurchaseIOSProps;
|
|
366
376
|
readonly android?: RequestSubscriptionAndroidProps;
|
|
367
377
|
}
|
|
368
378
|
|
|
@@ -378,4 +388,13 @@ export type RequestPurchaseProps = RequestPurchasePropsByPlatforms;
|
|
|
378
388
|
*/
|
|
379
389
|
export type RequestSubscriptionProps = RequestSubscriptionPropsByPlatforms;
|
|
380
390
|
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// Deprecated Aliases for Backward Compatibility
|
|
393
|
+
// ============================================================================
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @deprecated Use RequestPurchaseIOSProps instead. This alias will be removed in v3.0.0.
|
|
397
|
+
*/
|
|
398
|
+
export type RequestPurchaseIosProps = RequestPurchaseIOSProps;
|
|
399
|
+
|
|
381
400
|
// Note: Type guard functions are exported from index.ts to avoid conflicts
|
|
@@ -28,15 +28,16 @@ export const getActiveSubscriptions = async (
|
|
|
28
28
|
const filteredPurchases = purchases.filter((purchase) => {
|
|
29
29
|
// If specific IDs provided, filter by them
|
|
30
30
|
if (subscriptionIds && subscriptionIds.length > 0) {
|
|
31
|
-
if (!subscriptionIds.includes(purchase.
|
|
31
|
+
if (!subscriptionIds.includes(purchase.productId)) {
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Check if this purchase has subscription-specific fields
|
|
37
37
|
const hasSubscriptionFields =
|
|
38
|
-
('
|
|
39
|
-
'autoRenewingAndroid' in purchase
|
|
38
|
+
('expirationDateIOS' in purchase && purchase.expirationDateIOS) ||
|
|
39
|
+
'autoRenewingAndroid' in purchase ||
|
|
40
|
+
('environmentIOS' in purchase && purchase.environmentIOS === 'Sandbox');
|
|
40
41
|
|
|
41
42
|
if (!hasSubscriptionFields) {
|
|
42
43
|
return false;
|
|
@@ -44,19 +45,25 @@ export const getActiveSubscriptions = async (
|
|
|
44
45
|
|
|
45
46
|
// Check if it's actually active
|
|
46
47
|
if (Platform.OS === 'ios') {
|
|
47
|
-
if ('
|
|
48
|
-
return purchase.
|
|
48
|
+
if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
|
|
49
|
+
return purchase.expirationDateIOS > currentTime;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
) {
|
|
51
|
+
// For iOS purchases without expiration date (like Sandbox), we consider them active
|
|
52
|
+
// if they have the environmentIOS field and were created recently
|
|
53
|
+
if ('environmentIOS' in purchase && purchase.environmentIOS) {
|
|
54
54
|
const dayInMs = 24 * 60 * 60 * 1000;
|
|
55
|
+
// If no expiration date, consider active if transaction is recent (within 24 hours for Sandbox)
|
|
55
56
|
if (
|
|
56
|
-
purchase
|
|
57
|
-
|
|
57
|
+
!('expirationDateIOS' in purchase) ||
|
|
58
|
+
!purchase.expirationDateIOS
|
|
58
59
|
) {
|
|
59
|
-
|
|
60
|
+
if (
|
|
61
|
+
purchase.environmentIOS === 'Sandbox' &&
|
|
62
|
+
purchase.transactionDate &&
|
|
63
|
+
currentTime - purchase.transactionDate < dayInMs
|
|
64
|
+
) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
60
67
|
}
|
|
61
68
|
}
|
|
62
69
|
} else if (Platform.OS === 'android') {
|
|
@@ -70,26 +77,26 @@ export const getActiveSubscriptions = async (
|
|
|
70
77
|
// Convert to ActiveSubscription format
|
|
71
78
|
for (const purchase of filteredPurchases) {
|
|
72
79
|
const subscription: ActiveSubscription = {
|
|
73
|
-
productId: purchase.
|
|
80
|
+
productId: purchase.productId,
|
|
74
81
|
isActive: true,
|
|
75
82
|
};
|
|
76
83
|
|
|
77
84
|
// Add platform-specific details
|
|
78
85
|
if (Platform.OS === 'ios') {
|
|
79
|
-
if ('
|
|
80
|
-
const expirationDate = new Date(purchase.
|
|
86
|
+
if ('expirationDateIOS' in purchase && purchase.expirationDateIOS) {
|
|
87
|
+
const expirationDate = new Date(purchase.expirationDateIOS);
|
|
81
88
|
subscription.expirationDateIOS = expirationDate;
|
|
82
89
|
|
|
83
|
-
// Calculate days until expiration
|
|
84
|
-
const daysUntilExpiration = Math.
|
|
85
|
-
(purchase.
|
|
90
|
+
// Calculate days until expiration (round to nearest day)
|
|
91
|
+
const daysUntilExpiration = Math.round(
|
|
92
|
+
(purchase.expirationDateIOS - currentTime) / (1000 * 60 * 60 * 24),
|
|
86
93
|
);
|
|
87
94
|
subscription.daysUntilExpirationIOS = daysUntilExpiration;
|
|
88
95
|
subscription.willExpireSoon = daysUntilExpiration <= 7;
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
if ('
|
|
92
|
-
subscription.environmentIOS = purchase.
|
|
98
|
+
if ('environmentIOS' in purchase) {
|
|
99
|
+
subscription.environmentIOS = purchase.environmentIOS;
|
|
93
100
|
}
|
|
94
101
|
} else if (Platform.OS === 'android') {
|
|
95
102
|
if ('autoRenewingAndroid' in purchase) {
|
package/src/index.ts
CHANGED
|
@@ -5,9 +5,9 @@ import {Platform} from 'react-native';
|
|
|
5
5
|
// Internal modules
|
|
6
6
|
import ExpoIapModule from './ExpoIapModule';
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
isProductIOS,
|
|
9
9
|
validateReceiptIOS,
|
|
10
|
-
|
|
10
|
+
deepLinkToSubscriptionsIOS,
|
|
11
11
|
} from './modules/ios';
|
|
12
12
|
import {
|
|
13
13
|
isProductAndroid,
|
|
@@ -28,13 +28,13 @@ import {
|
|
|
28
28
|
SubscriptionPurchase,
|
|
29
29
|
} from './ExpoIap.types';
|
|
30
30
|
import {ProductPurchaseAndroid} from './types/ExpoIapAndroid.types';
|
|
31
|
-
import {PaymentDiscount} from './types/
|
|
31
|
+
import {PaymentDiscount} from './types/ExpoIapIOS.types';
|
|
32
32
|
|
|
33
33
|
// Export all types
|
|
34
34
|
export * from './ExpoIap.types';
|
|
35
35
|
export * from './modules/android';
|
|
36
36
|
export * from './modules/ios';
|
|
37
|
-
export type {AppTransactionIOS} from './types/
|
|
37
|
+
export type {AppTransactionIOS} from './types/ExpoIapIOS.types';
|
|
38
38
|
|
|
39
39
|
// Export subscription helpers
|
|
40
40
|
export {
|
|
@@ -135,7 +135,7 @@ export const getProducts = async (skus: string[]): Promise<Product[]> => {
|
|
|
135
135
|
ios: async () => {
|
|
136
136
|
const rawItems = await ExpoIapModule.getItems(skus);
|
|
137
137
|
return rawItems.filter((item: unknown) => {
|
|
138
|
-
if (!
|
|
138
|
+
if (!isProductIOS(item)) return false;
|
|
139
139
|
return (
|
|
140
140
|
typeof item === 'object' &&
|
|
141
141
|
item !== null &&
|
|
@@ -169,7 +169,7 @@ export const getSubscriptions = async (
|
|
|
169
169
|
ios: async () => {
|
|
170
170
|
const rawItems = await ExpoIapModule.getItems(skus);
|
|
171
171
|
return rawItems.filter((item: unknown) => {
|
|
172
|
-
if (!
|
|
172
|
+
if (!isProductIOS(item)) return false;
|
|
173
173
|
return (
|
|
174
174
|
typeof item === 'object' &&
|
|
175
175
|
item !== null &&
|
|
@@ -236,7 +236,7 @@ export const requestProducts = async ({
|
|
|
236
236
|
if (Platform.OS === 'ios') {
|
|
237
237
|
const rawItems = await ExpoIapModule.getItems(skus);
|
|
238
238
|
const filteredItems = rawItems.filter((item: unknown) => {
|
|
239
|
-
if (!
|
|
239
|
+
if (!isProductIOS(item)) return false;
|
|
240
240
|
return (
|
|
241
241
|
typeof item === 'object' &&
|
|
242
242
|
item !== null &&
|
|
@@ -341,7 +341,7 @@ export const getAvailablePurchases = ({
|
|
|
341
341
|
}) || (() => Promise.resolve([]))
|
|
342
342
|
)();
|
|
343
343
|
|
|
344
|
-
const
|
|
344
|
+
const offerToRecordIOS = (
|
|
345
345
|
offer: PaymentDiscount | undefined,
|
|
346
346
|
): Record<keyof PaymentDiscount, string> | undefined => {
|
|
347
347
|
if (!offer) return undefined;
|
|
@@ -436,7 +436,7 @@ export const requestPurchase = (
|
|
|
436
436
|
} = normalizedRequest;
|
|
437
437
|
|
|
438
438
|
return (async () => {
|
|
439
|
-
const offer =
|
|
439
|
+
const offer = offerToRecordIOS(withOffer);
|
|
440
440
|
const purchase = await ExpoIapModule.buyProduct(
|
|
441
441
|
sku,
|
|
442
442
|
andDangerouslyFinishTransactionAutomaticallyIOS,
|
|
@@ -491,13 +491,14 @@ export const requestPurchase = (
|
|
|
491
491
|
subscriptionOffers = [],
|
|
492
492
|
replacementModeAndroid = -1,
|
|
493
493
|
purchaseTokenAndroid,
|
|
494
|
+
purchaseToken,
|
|
494
495
|
} = normalizedRequest;
|
|
495
496
|
|
|
496
497
|
return (async () => {
|
|
497
498
|
return ExpoIapModule.buyItemByType({
|
|
498
499
|
type: 'subs',
|
|
499
500
|
skuArr: skus,
|
|
500
|
-
purchaseToken: purchaseTokenAndroid,
|
|
501
|
+
purchaseToken: purchaseTokenAndroid || purchaseToken,
|
|
501
502
|
replacementMode: replacementModeAndroid,
|
|
502
503
|
obfuscatedAccountId: obfuscatedAccountIdAndroid,
|
|
503
504
|
obfuscatedProfileId: obfuscatedProfileIdAndroid,
|
|
@@ -575,20 +576,11 @@ export const finishTransaction = ({
|
|
|
575
576
|
android: async () => {
|
|
576
577
|
const androidPurchase = purchase as ProductPurchaseAndroid;
|
|
577
578
|
|
|
578
|
-
if (!('purchaseTokenAndroid' in androidPurchase)) {
|
|
579
|
-
return Promise.reject(
|
|
580
|
-
new Error('purchaseToken is required to finish transaction'),
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
579
|
if (isConsumable) {
|
|
584
|
-
return ExpoIapModule.consumeProduct(
|
|
585
|
-
androidPurchase.purchaseTokenAndroid,
|
|
586
|
-
);
|
|
587
|
-
} else {
|
|
588
|
-
return ExpoIapModule.acknowledgePurchase(
|
|
589
|
-
androidPurchase.purchaseTokenAndroid,
|
|
590
|
-
);
|
|
580
|
+
return ExpoIapModule.consumeProduct(androidPurchase.purchaseToken);
|
|
591
581
|
}
|
|
582
|
+
|
|
583
|
+
return ExpoIapModule.acknowledgePurchase(androidPurchase.purchaseToken);
|
|
592
584
|
},
|
|
593
585
|
}) || (() => Promise.reject(new Error('Unsupported Platform')))
|
|
594
586
|
)();
|
|
@@ -691,7 +683,7 @@ export const deepLinkToSubscriptions = (options: {
|
|
|
691
683
|
packageNameAndroid?: string;
|
|
692
684
|
}): Promise<void> => {
|
|
693
685
|
if (Platform.OS === 'ios') {
|
|
694
|
-
return
|
|
686
|
+
return deepLinkToSubscriptionsIOS();
|
|
695
687
|
}
|
|
696
688
|
|
|
697
689
|
if (Platform.OS === 'android') {
|
|
@@ -718,5 +710,5 @@ export const deepLinkToSubscriptions = (options: {
|
|
|
718
710
|
return Promise.reject(new Error(`Unsupported platform: ${Platform.OS}`));
|
|
719
711
|
};
|
|
720
712
|
|
|
721
|
-
export * from './
|
|
713
|
+
export * from './useIAP';
|
|
722
714
|
export * from './utils/errorMapping';
|