expo-iap 2.7.14 → 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/build/ExpoIap.types.d.ts +19 -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.js +8 -8
- 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/{ExpoIapIos.types.d.ts → ExpoIapIOS.types.d.ts} +41 -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 -46
- package/jest.config.js +43 -0
- package/package.json +8 -3
- package/src/ExpoIap.types.ts +29 -15
- package/src/helpers/subscription.ts +27 -20
- package/src/index.ts +11 -11
- package/src/modules/ios.ts +13 -40
- package/src/types/{ExpoIapIos.types.ts → ExpoIapIOS.types.ts} +42 -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,47 +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["
|
|
83
|
-
purchaseMap["purchaseToken"] =
|
|
80
|
+
if (jwsRepresentationIOS != nil) {
|
|
81
|
+
logDebug("serializeTransaction adding jwsRepresentationIOS with length: \(jwsRepresentationIOS!.count)")
|
|
82
|
+
purchaseMap["jwsRepresentationIOS"] = jwsRepresentationIOS
|
|
83
|
+
purchaseMap["purchaseToken"] = jwsRepresentationIOS
|
|
84
84
|
} else {
|
|
85
|
-
logDebug("serializeTransaction
|
|
85
|
+
logDebug("serializeTransaction jwsRepresentationIOS is nil")
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if #available(iOS 16.0, *) {
|
|
89
|
-
purchaseMap["
|
|
89
|
+
purchaseMap["environmentIOS"] = transaction.environment.rawValue
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if #available(iOS 17.0, *) {
|
|
93
|
-
purchaseMap["
|
|
94
|
-
purchaseMap["
|
|
93
|
+
purchaseMap["storefrontCountryCodeIOS"] = transaction.storefront.countryCode
|
|
94
|
+
purchaseMap["reasonIOS"] = transaction.reason.rawValue
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
if #available(iOS 17.2, *) {
|
|
98
98
|
if let offer = transaction.offer {
|
|
99
|
-
purchaseMap["
|
|
99
|
+
purchaseMap["offerIOS"] = [
|
|
100
100
|
"id": offer.id ?? "",
|
|
101
101
|
"type": offer.type.rawValue,
|
|
102
102
|
"paymentMode": offer.paymentMode?.rawValue ?? "",
|
|
@@ -106,10 +106,10 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
106
106
|
|
|
107
107
|
if #available(iOS 15.4, *), let jsonData = jsonData {
|
|
108
108
|
if let price = jsonData["price"] as? NSNumber {
|
|
109
|
-
purchaseMap["
|
|
109
|
+
purchaseMap["priceIOS"] = price.doubleValue
|
|
110
110
|
}
|
|
111
111
|
if let currency = jsonData["currency"] as? String {
|
|
112
|
-
purchaseMap["
|
|
112
|
+
purchaseMap["currencyIOS"] = currency
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -118,7 +118,7 @@ func serializeTransaction(_ transaction: Transaction, jwsRepresentationIos: Stri
|
|
|
118
118
|
|
|
119
119
|
private let DEFAULT_SUBSCRIPTION_PERIOD_UNIT = "DAY" // Default fallback unit for subscription periods.
|
|
120
120
|
|
|
121
|
-
func
|
|
121
|
+
func getPeriodIOS(_ unit: Product.SubscriptionPeriod.Unit) -> String {
|
|
122
122
|
return switch (unit) {
|
|
123
123
|
case .day: "DAY"
|
|
124
124
|
case .week: "WEEK"
|
|
@@ -135,7 +135,7 @@ func serializeOffer(_ offer: Product.SubscriptionOffer?) -> [String: Any?]? {
|
|
|
135
135
|
return [
|
|
136
136
|
"id": offer.id,
|
|
137
137
|
"period": [
|
|
138
|
-
"unit":
|
|
138
|
+
"unit": getPeriodIOS(offer.period.unit),
|
|
139
139
|
"value": offer.period.value
|
|
140
140
|
],
|
|
141
141
|
"periodCount": offer.periodCount,
|
|
@@ -151,9 +151,9 @@ func serializeSubscription(_ s: Product.SubscriptionInfo?) -> [String: Any?]? {
|
|
|
151
151
|
return [
|
|
152
152
|
"introductoryOffer": serializeOffer(s.introductoryOffer),
|
|
153
153
|
"promotionalOffers": s.promotionalOffers.map(serializeOffer),
|
|
154
|
-
"
|
|
154
|
+
"subscriptionGroupId": s.subscriptionGroupID,
|
|
155
155
|
"subscriptionPeriod": [
|
|
156
|
-
"unit":
|
|
156
|
+
"unit": getPeriodIOS(s.subscriptionPeriod.unit),
|
|
157
157
|
"value": s.subscriptionPeriod.value
|
|
158
158
|
],
|
|
159
159
|
]
|
|
@@ -290,7 +290,7 @@ public class ExpoIapModule: Module {
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
var result: [String: Any?] = [
|
|
293
|
-
"
|
|
293
|
+
"bundleId": appTransaction.bundleID,
|
|
294
294
|
"appVersion": appTransaction.appVersion,
|
|
295
295
|
"originalAppVersion": appTransaction.originalAppVersion,
|
|
296
296
|
"originalPurchaseDate": appTransaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
@@ -298,13 +298,13 @@ public class ExpoIapModule: Module {
|
|
|
298
298
|
"deviceVerificationNonce": appTransaction.deviceVerificationNonce.uuidString,
|
|
299
299
|
"environment": appTransaction.environment.rawValue,
|
|
300
300
|
"signedDate": appTransaction.signedDate.timeIntervalSince1970 * 1000,
|
|
301
|
-
"
|
|
302
|
-
"
|
|
301
|
+
"appId": appTransaction.appID,
|
|
302
|
+
"appVersionId": appTransaction.appVersionID,
|
|
303
303
|
"preorderDate": appTransaction.preorderDate.map { $0.timeIntervalSince1970 * 1000 }
|
|
304
304
|
]
|
|
305
305
|
|
|
306
306
|
if #available(iOS 18.4, *) {
|
|
307
|
-
result["
|
|
307
|
+
result["appTransactionId"] = appTransaction.appTransactionID
|
|
308
308
|
result["originalPlatform"] = appTransaction.originalPlatform.rawValue
|
|
309
309
|
}
|
|
310
310
|
|
|
@@ -393,8 +393,8 @@ public class ExpoIapModule: Module {
|
|
|
393
393
|
|
|
394
394
|
var purchasedItemsSerialized: [[String: Any?]] = []
|
|
395
395
|
|
|
396
|
-
func addTransaction(transaction: Transaction,
|
|
397
|
-
let serialized = serializeTransaction(transaction,
|
|
396
|
+
func addTransaction(transaction: Transaction, jwsRepresentationIOS: String? = nil) {
|
|
397
|
+
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: jwsRepresentationIOS)
|
|
398
398
|
purchasedItemsSerialized.append(serialized)
|
|
399
399
|
|
|
400
400
|
if alsoPublishToEventListener {
|
|
@@ -408,7 +408,7 @@ public class ExpoIapModule: Module {
|
|
|
408
408
|
do {
|
|
409
409
|
let transaction = try self.checkVerified(verification)
|
|
410
410
|
if !onlyIncludeActiveItems {
|
|
411
|
-
addTransaction(transaction: transaction,
|
|
411
|
+
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
412
412
|
continue
|
|
413
413
|
}
|
|
414
414
|
switch transaction.productType {
|
|
@@ -416,7 +416,7 @@ public class ExpoIapModule: Module {
|
|
|
416
416
|
if await self.productStore?.getProduct(productID: transaction.productID)
|
|
417
417
|
!= nil
|
|
418
418
|
{
|
|
419
|
-
addTransaction(transaction: transaction,
|
|
419
|
+
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
420
420
|
}
|
|
421
421
|
case .nonRenewable:
|
|
422
422
|
if await self.productStore?.getProduct(productID: transaction.productID)
|
|
@@ -426,7 +426,7 @@ public class ExpoIapModule: Module {
|
|
|
426
426
|
let expirationDate = Calendar(identifier: .gregorian).date(
|
|
427
427
|
byAdding: DateComponents(year: 1), to: transaction.purchaseDate)!
|
|
428
428
|
if currentDate < expirationDate {
|
|
429
|
-
addTransaction(transaction: transaction,
|
|
429
|
+
addTransaction(transaction: transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
432
|
default:
|
|
@@ -537,10 +537,10 @@ public class ExpoIapModule: Module {
|
|
|
537
537
|
return nil
|
|
538
538
|
} else {
|
|
539
539
|
self.transactions[String(transaction.id)] = transaction
|
|
540
|
-
let serialized = serializeTransaction(transaction,
|
|
540
|
+
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: verification.jwsRepresentation)
|
|
541
541
|
|
|
542
|
-
// Debug: Check if
|
|
543
|
-
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)")
|
|
544
544
|
|
|
545
545
|
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
546
546
|
return serialized
|
|
@@ -888,7 +888,7 @@ public class ExpoIapModule: Module {
|
|
|
888
888
|
// If this doesn't throw, the transaction is verified
|
|
889
889
|
let transaction = try self.checkVerified(result)
|
|
890
890
|
isValid = true
|
|
891
|
-
latestTransaction = serializeTransaction(transaction,
|
|
891
|
+
latestTransaction = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
892
892
|
} catch {
|
|
893
893
|
isValid = false
|
|
894
894
|
}
|
|
@@ -974,7 +974,7 @@ public class ExpoIapModule: Module {
|
|
|
974
974
|
let transaction = try self.checkVerified(result)
|
|
975
975
|
self.transactions[String(transaction.id)] = transaction
|
|
976
976
|
if self.hasListeners {
|
|
977
|
-
let serialized = serializeTransaction(transaction,
|
|
977
|
+
let serialized = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
978
978
|
self.sendEvent(IapEvent.PurchaseUpdated, serialized)
|
|
979
979
|
}
|
|
980
980
|
} catch {
|
|
@@ -1076,7 +1076,7 @@ public class ExpoIapModule: Module {
|
|
|
1076
1076
|
previousWillAutoRenew != currentWillAutoRenew {
|
|
1077
1077
|
|
|
1078
1078
|
// Use the jwsRepresentation when serializing the transaction
|
|
1079
|
-
var purchaseMap = serializeTransaction(transaction,
|
|
1079
|
+
var purchaseMap = serializeTransaction(transaction, jwsRepresentationIOS: result.jwsRepresentation)
|
|
1080
1080
|
|
|
1081
1081
|
if case .verified(let renewalInfo) = status.renewalInfo {
|
|
1082
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 = {
|
|
@@ -43,29 +43,34 @@ export type AndroidPlatform = {platform: 'android'};
|
|
|
43
43
|
// Platform-agnostic unified product types (public API)
|
|
44
44
|
export type Product =
|
|
45
45
|
| (ProductAndroid & AndroidPlatform)
|
|
46
|
-
| (
|
|
46
|
+
| (ProductIOS & IosPlatform);
|
|
47
47
|
|
|
48
48
|
export type SubscriptionProduct =
|
|
49
49
|
| (SubscriptionProductAndroid & AndroidPlatform)
|
|
50
|
-
| (
|
|
50
|
+
| (SubscriptionProductIOS & IosPlatform);
|
|
51
51
|
|
|
52
52
|
// ============================================================================
|
|
53
|
-
// Legacy Types (For backward compatibility with
|
|
53
|
+
// Legacy Types (For backward compatibility with useIAP hook)
|
|
54
54
|
// ============================================================================
|
|
55
55
|
|
|
56
56
|
// Re-export platform-specific purchase types for legacy compatibility
|
|
57
57
|
export type {ProductPurchaseAndroid} from './types/ExpoIapAndroid.types';
|
|
58
|
-
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;
|
|
59
64
|
|
|
60
65
|
// Union type for platform-specific purchase types (legacy support)
|
|
61
66
|
export type ProductPurchase =
|
|
62
67
|
| (ProductPurchaseAndroid & AndroidPlatform)
|
|
63
|
-
| (
|
|
68
|
+
| (ProductPurchaseIOS & IosPlatform);
|
|
64
69
|
|
|
65
70
|
// Union type for platform-specific subscription purchase types (legacy support)
|
|
66
71
|
export type SubscriptionPurchase =
|
|
67
72
|
| (ProductPurchaseAndroid & AndroidPlatform & {autoRenewingAndroid: boolean})
|
|
68
|
-
| (
|
|
73
|
+
| (ProductPurchaseIOS & IosPlatform);
|
|
69
74
|
|
|
70
75
|
export type Purchase = ProductPurchase | SubscriptionPurchase;
|
|
71
76
|
|
|
@@ -308,7 +313,7 @@ export interface UnifiedRequestPurchaseProps {
|
|
|
308
313
|
readonly andDangerouslyFinishTransactionAutomaticallyIOS?: boolean;
|
|
309
314
|
readonly appAccountToken?: string;
|
|
310
315
|
readonly quantity?: number;
|
|
311
|
-
readonly withOffer?: import('./types/
|
|
316
|
+
readonly withOffer?: import('./types/ExpoIapIOS.types').PaymentDiscount;
|
|
312
317
|
|
|
313
318
|
// Android-specific properties (ignored on iOS)
|
|
314
319
|
readonly obfuscatedAccountIdAndroid?: string;
|
|
@@ -323,12 +328,12 @@ export interface UnifiedRequestPurchaseProps {
|
|
|
323
328
|
/**
|
|
324
329
|
* iOS-specific purchase request parameters
|
|
325
330
|
*/
|
|
326
|
-
export interface
|
|
331
|
+
export interface RequestPurchaseIOSProps {
|
|
327
332
|
readonly sku: string;
|
|
328
333
|
readonly andDangerouslyFinishTransactionAutomaticallyIOS?: boolean;
|
|
329
334
|
readonly appAccountToken?: string;
|
|
330
335
|
readonly quantity?: number;
|
|
331
|
-
readonly withOffer?: import('./types/
|
|
336
|
+
readonly withOffer?: import('./types/ExpoIapIOS.types').PaymentDiscount;
|
|
332
337
|
}
|
|
333
338
|
|
|
334
339
|
/**
|
|
@@ -359,7 +364,7 @@ export interface RequestSubscriptionAndroidProps
|
|
|
359
364
|
* Allows clear separation of iOS and Android parameters
|
|
360
365
|
*/
|
|
361
366
|
export interface RequestPurchasePropsByPlatforms {
|
|
362
|
-
readonly ios?:
|
|
367
|
+
readonly ios?: RequestPurchaseIOSProps;
|
|
363
368
|
readonly android?: RequestPurchaseAndroidProps;
|
|
364
369
|
}
|
|
365
370
|
|
|
@@ -367,7 +372,7 @@ export interface RequestPurchasePropsByPlatforms {
|
|
|
367
372
|
* Modern platform-specific subscription request structure (v2.7.0+)
|
|
368
373
|
*/
|
|
369
374
|
export interface RequestSubscriptionPropsByPlatforms {
|
|
370
|
-
readonly ios?:
|
|
375
|
+
readonly ios?: RequestPurchaseIOSProps;
|
|
371
376
|
readonly android?: RequestSubscriptionAndroidProps;
|
|
372
377
|
}
|
|
373
378
|
|
|
@@ -383,4 +388,13 @@ export type RequestPurchaseProps = RequestPurchasePropsByPlatforms;
|
|
|
383
388
|
*/
|
|
384
389
|
export type RequestSubscriptionProps = RequestSubscriptionPropsByPlatforms;
|
|
385
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
|
+
|
|
386
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,
|
|
@@ -683,7 +683,7 @@ export const deepLinkToSubscriptions = (options: {
|
|
|
683
683
|
packageNameAndroid?: string;
|
|
684
684
|
}): Promise<void> => {
|
|
685
685
|
if (Platform.OS === 'ios') {
|
|
686
|
-
return
|
|
686
|
+
return deepLinkToSubscriptionsIOS();
|
|
687
687
|
}
|
|
688
688
|
|
|
689
689
|
if (Platform.OS === 'android') {
|
|
@@ -710,5 +710,5 @@ export const deepLinkToSubscriptions = (options: {
|
|
|
710
710
|
return Promise.reject(new Error(`Unsupported platform: ${Platform.OS}`));
|
|
711
711
|
};
|
|
712
712
|
|
|
713
|
-
export * from './
|
|
713
|
+
export * from './useIAP';
|
|
714
714
|
export * from './utils/errorMapping';
|