expo-iap 2.1.0 → 2.2.0-rc.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/README.md +8 -27
- package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +10 -9
- package/build/ExpoIap.types.d.ts +31 -40
- package/build/ExpoIap.types.d.ts.map +1 -1
- package/build/ExpoIap.types.js +0 -11
- package/build/ExpoIap.types.js.map +1 -1
- package/build/index.d.ts +2 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +32 -46
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +12 -4
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +8 -4
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +15 -7
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +9 -5
- package/build/modules/ios.js.map +1 -1
- package/build/types/ExpoIapAndroid.types.d.ts +31 -31
- package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
- package/build/types/ExpoIapAndroid.types.js +6 -0
- package/build/types/ExpoIapAndroid.types.js.map +1 -1
- package/build/types/ExpoIapIos.types.d.ts +31 -34
- package/build/types/ExpoIapIos.types.d.ts.map +1 -1
- package/build/types/ExpoIapIos.types.js.map +1 -1
- package/build/useIap.d.ts +1 -2
- package/build/useIap.d.ts.map +1 -1
- package/build/useIap.js +3 -3
- package/build/useIap.js.map +1 -1
- package/bun.lockb +0 -0
- package/iap.md +161 -0
- package/ios/ExpoIapModule.swift +116 -30
- package/package.json +5 -5
- package/plugin/build/withIAP.js +29 -2
- package/plugin/src/withIAP.ts +41 -4
- package/src/ExpoIap.types.ts +48 -48
- package/src/index.ts +48 -96
- package/src/modules/android.ts +16 -12
- package/src/modules/ios.ts +20 -17
- package/src/types/ExpoIapAndroid.types.ts +37 -33
- package/src/types/ExpoIapIos.types.ts +36 -40
- package/src/useIap.ts +5 -8
package/iap.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Expo IAP Documentation
|
|
2
|
+
|
|
3
|
+
## Installation in Managed Expo Projects
|
|
4
|
+
|
|
5
|
+
For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, follow the installation instructions in the [API documentation for the latest stable release](#api-documentation). If the link shows no documentation, this library isn't yet supported in managed workflows—it's likely awaiting inclusion in a future Expo SDK release.
|
|
6
|
+
|
|
7
|
+
## Installation in Bare React Native Projects
|
|
8
|
+
|
|
9
|
+
For bare React Native projects, ensure the [`expo` package is installed and configured](https://docs.expo.dev/bare/installing-expo-modules/) before proceeding.
|
|
10
|
+
|
|
11
|
+
### Add the Package to Your npm Dependencies
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install expo-iap
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Configure for iOS
|
|
18
|
+
|
|
19
|
+
Run `npx pod-install` after installing the npm package. Since `expo-iap` uses `StoreKit2`, set the `deploymentTarget` to `15.0` or higher in your project configuration.
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
"ios": {
|
|
23
|
+
"deploymentTarget": "15.0"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Configure for Android
|
|
28
|
+
|
|
29
|
+
No additional configuration is required beyond installing the package, as `expo-iap` leverages Google Play Billing internally.
|
|
30
|
+
|
|
31
|
+
## IAP Types
|
|
32
|
+
|
|
33
|
+
`expo-iap` supports the following In-App Purchase types, aligned with platform-specific APIs (Google Play Billing for Android, StoreKit2 for iOS).
|
|
34
|
+
|
|
35
|
+
### Consumable
|
|
36
|
+
|
|
37
|
+
- **Description**: Items that are used up after purchase and can be bought again (e.g., in-game currency, consumable boosts).
|
|
38
|
+
- **Behavior**: Requires consumption acknowledgment after purchase to allow repurchasing.
|
|
39
|
+
- **Platforms**: Supported on both Android and iOS.
|
|
40
|
+
|
|
41
|
+
### Non-Consumable
|
|
42
|
+
|
|
43
|
+
- **Description**: One-time purchases that users own permanently (e.g., ad removal, premium features).
|
|
44
|
+
- **Behavior**: Supports restoration of previous purchases; cannot be repurchased.
|
|
45
|
+
- **Platforms**: Supported on both Android and iOS.
|
|
46
|
+
|
|
47
|
+
### Subscription
|
|
48
|
+
|
|
49
|
+
- **Description**: Recurring purchases for ongoing access to content or services (e.g., monthly premium membership).
|
|
50
|
+
- **Behavior**: Includes auto-renewing options and restore functionality.
|
|
51
|
+
- **Platforms**: Supported on both Android and iOS, with platform-specific subscription details.
|
|
52
|
+
|
|
53
|
+
## Product Type
|
|
54
|
+
|
|
55
|
+
This section outlines the properties of products supported by `expo-iap`, including common fields shared across platforms and platform-specific extensions.
|
|
56
|
+
|
|
57
|
+
### Common Product Types
|
|
58
|
+
|
|
59
|
+
These properties are shared between Android and iOS, defined in `BaseProduct`.
|
|
60
|
+
|
|
61
|
+
| Property | Type | Description |
|
|
62
|
+
| -------------- | ------------- | --------------------------------- |
|
|
63
|
+
| `id` | `string` | Unique identifier for the product |
|
|
64
|
+
| `title` | `string` | Title of the product |
|
|
65
|
+
| `description` | `string` | Description of the product |
|
|
66
|
+
| `type` | `ProductType` | Product type (inapp or subs) |
|
|
67
|
+
| `displayName` | `string?` | Name for UI display (optional) |
|
|
68
|
+
| `displayPrice` | `string?` | Display price (optional) |
|
|
69
|
+
| `price` | `number?` | Actual price (optional) |
|
|
70
|
+
| `currency` | `string?` | Currency code (optional) |
|
|
71
|
+
|
|
72
|
+
### Android-Only Product Types
|
|
73
|
+
|
|
74
|
+
- **`ProductAndroid`**
|
|
75
|
+
|
|
76
|
+
- `name: string`: Product name (used instead of `displayName` on Android).
|
|
77
|
+
- `oneTimePurchaseOfferDetails?: OneTimePurchaseOfferDetails`: Details for one-time purchases (e.g., price, currency).
|
|
78
|
+
- `subscriptionOfferDetails?: SubscriptionOfferDetail[]`: Subscription offer details.
|
|
79
|
+
|
|
80
|
+
- **`SubscriptionProductAndroid`**
|
|
81
|
+
- `subscriptionOfferDetails: SubscriptionOfferAndroid[]`: Subscription-specific offers, including base plan ID, offer token, and pricing phases.
|
|
82
|
+
|
|
83
|
+
### iOS-Only Product Types
|
|
84
|
+
|
|
85
|
+
- **`ProductIos`**
|
|
86
|
+
|
|
87
|
+
- `displayPrice: string`: Price formatted for display.
|
|
88
|
+
- `isFamilyShareable: boolean`: Indicates if the product supports family sharing.
|
|
89
|
+
- `jsonRepresentation: string`: JSON representation from StoreKit2.
|
|
90
|
+
- `subscription: SubscriptionInfo`: Subscription details (e.g., introductory offers, group ID).
|
|
91
|
+
|
|
92
|
+
- **`SubscriptionProductIos`**
|
|
93
|
+
- `discounts?: Discount[]`: Discount details (e.g., identifier, price).
|
|
94
|
+
- `introductoryPrice?: string`: Introductory pricing details (with additional iOS-specific fields like `introductoryPricePaymentModeIos`).
|
|
95
|
+
|
|
96
|
+
## Purchase Type
|
|
97
|
+
|
|
98
|
+
This section describes the properties of purchases supported by `expo-iap`, covering common fields and platform-specific extensions.
|
|
99
|
+
|
|
100
|
+
### Common Purchase Types
|
|
101
|
+
|
|
102
|
+
These properties are shared between Android and iOS, defined in `ProductPurchase`.
|
|
103
|
+
|
|
104
|
+
| Property | Type | Description |
|
|
105
|
+
| -------------------- | --------- | ---------------------------------------- |
|
|
106
|
+
| `id` | `string` | ID of the purchased product |
|
|
107
|
+
| `transactionId` | `string?` | Unique transaction identifier (optional) |
|
|
108
|
+
| `transactionDate` | `number` | Purchase timestamp (Unix) |
|
|
109
|
+
| `transactionReceipt` | `string` | Transaction receipt data |
|
|
110
|
+
| `purchaseToken` | `string?` | Purchase token (optional) |
|
|
111
|
+
|
|
112
|
+
### Android-Only Purchase Types
|
|
113
|
+
|
|
114
|
+
- **`ProductPurchase` (Android Extensions)**
|
|
115
|
+
|
|
116
|
+
- `ids?: string[]`: List of purchased product IDs.
|
|
117
|
+
- `dataAndroid?: string`: Payment data.
|
|
118
|
+
- `signatureAndroid?: string`: Signature data.
|
|
119
|
+
- `autoRenewingAndroid?: boolean`: Auto-renewal status.
|
|
120
|
+
- `purchaseStateAndroid?: PurchaseStateAndroid`: Purchase state (e.g., PURCHASED, PENDING).
|
|
121
|
+
|
|
122
|
+
- **`SubscriptionPurchase` (Android Extensions)**
|
|
123
|
+
- `autoRenewingAndroid?: boolean`: Subscription auto-renewal status.
|
|
124
|
+
|
|
125
|
+
### iOS-Only Purchase Types
|
|
126
|
+
|
|
127
|
+
- **`ProductPurchase` (iOS Extensions)**
|
|
128
|
+
|
|
129
|
+
- `quantityIos?: number`: Purchase quantity.
|
|
130
|
+
- `originalTransactionDateIos?: number`: Original transaction date.
|
|
131
|
+
- `originalTransactionIdentifierIos?: string`: Original transaction ID.
|
|
132
|
+
- `verificationResultIos?: string`: Verification result.
|
|
133
|
+
- `appAccountToken?: string`: App account token.
|
|
134
|
+
- `expirationDateIos?: number`: Expiration date for subscriptions.
|
|
135
|
+
- `webOrderLineItemIdIos?: number`: Web order line item ID.
|
|
136
|
+
- `environmentIos?: string`: App Store environment.
|
|
137
|
+
- `storefrontCountryCodeIos?: string`: App Store storefront country code.
|
|
138
|
+
- `appBundleIdIos?: string`: App bundle ID.
|
|
139
|
+
- `productTypeIos?: string`: Product type (e.g., "autoRenewable").
|
|
140
|
+
- `subscriptionGroupIdIos?: string`: Subscription group ID.
|
|
141
|
+
- `isUpgradedIos?: boolean`: Whether the subscription was upgraded.
|
|
142
|
+
- `ownershipTypeIos?: string`: Ownership type (e.g., individual, family sharing).
|
|
143
|
+
- `reasonIos?: string`: Transaction reason.
|
|
144
|
+
- `transactionReasonIos?: string`: Detailed transaction reason.
|
|
145
|
+
- `revocationDateIos?: number`: Date of revocation if refunded.
|
|
146
|
+
- `revocationReasonIos?: string`: Reason for revocation.
|
|
147
|
+
|
|
148
|
+
- **`SubscriptionPurchase` (iOS Extensions)**
|
|
149
|
+
- All the iOS-specific fields from `ProductPurchase`
|
|
150
|
+
- Automatic subscription-specific handling based on product type
|
|
151
|
+
|
|
152
|
+
## Implementation Notes
|
|
153
|
+
|
|
154
|
+
### Platform-Uniform Purchase Handling
|
|
155
|
+
|
|
156
|
+
`expo-iap` now processes transactions directly to `Purchase` or `SubscriptionPurchase` types on both platforms:
|
|
157
|
+
|
|
158
|
+
- **iOS**: StoreKit 2 transactions are directly mapped to `Purchase`/`SubscriptionPurchase` objects with iOS-specific fields using camelCase naming convention (e.g., `expirationDateIos`).
|
|
159
|
+
- **Android**: Google Play Billing purchases are similarly mapped to the same types with Android-specific fields.
|
|
160
|
+
|
|
161
|
+
This approach eliminates intermediate conversion layers, making the code more maintainable while still providing access to platform-specific details when needed.
|
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -15,6 +15,109 @@ struct IapEvent {
|
|
|
15
15
|
static let TransactionIapUpdated = "iap-transaction-updated"
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
@available(iOS 15.0, *)
|
|
19
|
+
func serializeTransaction(_ transaction: Transaction) -> [String: Any?] {
|
|
20
|
+
// Determine if this is a subscription by productType or expirationDate
|
|
21
|
+
let isSubscription = transaction.productType.rawValue.lowercased().contains("renewable") || transaction.expirationDate != nil
|
|
22
|
+
|
|
23
|
+
// Parse transaction reason from jsonRepresentation if available
|
|
24
|
+
var transactionReasonIos: String? = nil
|
|
25
|
+
var webOrderLineItemId: Int? = nil
|
|
26
|
+
var jsonData: [String: Any]? = nil
|
|
27
|
+
|
|
28
|
+
// JSON representation handling
|
|
29
|
+
do {
|
|
30
|
+
let jsonRep = transaction.jsonRepresentation
|
|
31
|
+
let jsonObj = try JSONSerialization.jsonObject(with: jsonRep) as! [String: Any]
|
|
32
|
+
jsonData = jsonObj
|
|
33
|
+
if let reason = jsonObj["transactionReason"] as? String {
|
|
34
|
+
transactionReasonIos = reason
|
|
35
|
+
}
|
|
36
|
+
if let webOrderId = jsonObj["webOrderLineItemID"] as? NSNumber {
|
|
37
|
+
webOrderLineItemId = webOrderId.intValue
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
print("Error parsing JSON representation: \(error)")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create base purchase object that matches Purchase type in TypeScript
|
|
44
|
+
var purchaseMap: [String: Any?] = [
|
|
45
|
+
// Core purchase fields
|
|
46
|
+
"id": transaction.productID,
|
|
47
|
+
"ids": [transaction.productID],
|
|
48
|
+
"transactionId": String(transaction.id),
|
|
49
|
+
"transactionDate": transaction.purchaseDate.timeIntervalSince1970 * 1000,
|
|
50
|
+
"transactionReceipt": "", // Not available in StoreKit 2
|
|
51
|
+
"purchaseToken": "", // Not applicable on iOS
|
|
52
|
+
|
|
53
|
+
// iOS specific fields - basic info
|
|
54
|
+
"quantityIos": transaction.purchasedQuantity,
|
|
55
|
+
"originalTransactionDateIos": transaction.originalPurchaseDate.timeIntervalSince1970 * 1000,
|
|
56
|
+
"originalTransactionIdentifierIos": transaction.originalID,
|
|
57
|
+
"appAccountToken": transaction.appAccountToken?.uuidString,
|
|
58
|
+
|
|
59
|
+
// App and Product Identifiers
|
|
60
|
+
"appBundleIdIos": transaction.appBundleID,
|
|
61
|
+
"productTypeIos": transaction.productType.rawValue,
|
|
62
|
+
"subscriptionGroupIdIos": transaction.subscriptionGroupID,
|
|
63
|
+
|
|
64
|
+
// Transaction Identifiers
|
|
65
|
+
"webOrderLineItemIdIos": webOrderLineItemId,
|
|
66
|
+
|
|
67
|
+
// Purchase and Expiration Dates
|
|
68
|
+
"expirationDateIos": transaction.expirationDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
69
|
+
|
|
70
|
+
// Purchase Details
|
|
71
|
+
"isUpgradedIos": transaction.isUpgraded,
|
|
72
|
+
"ownershipTypeIos": transaction.ownershipType.rawValue,
|
|
73
|
+
|
|
74
|
+
// Revocation Status
|
|
75
|
+
"revocationDateIos": transaction.revocationDate.map { $0.timeIntervalSince1970 * 1000 },
|
|
76
|
+
"revocationReasonIos": transaction.revocationReason?.rawValue
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
// Environment (iOS 16.0+)
|
|
80
|
+
if #available(iOS 16.0, *) {
|
|
81
|
+
purchaseMap["environmentIos"] = transaction.environment.rawValue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Storefront (iOS 17.0+)
|
|
85
|
+
if #available(iOS 17.0, *) {
|
|
86
|
+
purchaseMap["storefrontCountryCodeIos"] = transaction.storefront.countryCode
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Transaction Reason (iOS 17.0+)
|
|
90
|
+
if #available(iOS 17.0, *) {
|
|
91
|
+
purchaseMap["reasonIos"] = transaction.reason.rawValue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// reasonStringRepresentation과 transactionReasonIos는 명시적 타입 처리
|
|
95
|
+
purchaseMap["reasonStringRepresentationIos"] = transaction.reasonStringRepresentation
|
|
96
|
+
purchaseMap["transactionReasonIos"] = transactionReasonIos
|
|
97
|
+
|
|
98
|
+
// Add offer information if available with proper availability check
|
|
99
|
+
if #available(iOS 17.2, *) {
|
|
100
|
+
if let offer = transaction.offer {
|
|
101
|
+
purchaseMap["offerIos"] = [
|
|
102
|
+
"id": offer.id as Any,
|
|
103
|
+
"type": offer.type.rawValue,
|
|
104
|
+
"paymentMode": offer.paymentMode?.rawValue ?? ""
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add additional pricing info if available
|
|
110
|
+
if #available(iOS 15.4, *), let priceInfo = jsonData?["price"] as? NSNumber {
|
|
111
|
+
purchaseMap["priceIos"] = priceInfo.doubleValue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if #available(iOS 15.4, *), let currencyInfo = jsonData?["currency"] as? String {
|
|
115
|
+
purchaseMap["currencyIos"] = currencyInfo
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return purchaseMap
|
|
119
|
+
}
|
|
120
|
+
|
|
18
121
|
@available(iOS 15.0, *)
|
|
19
122
|
func serializeProduct(_ p: Product) -> [String: Any?] {
|
|
20
123
|
return [
|
|
@@ -23,24 +126,26 @@ func serializeProduct(_ p: Product) -> [String: Any?] {
|
|
|
23
126
|
"displayName": p.displayName,
|
|
24
127
|
"displayPrice": p.displayPrice,
|
|
25
128
|
"id": p.id,
|
|
129
|
+
"title": p.displayName,
|
|
26
130
|
"isFamilyShareable": p.isFamilyShareable,
|
|
27
131
|
"jsonRepresentation": serializeDebug(String(data: p.jsonRepresentation, encoding: .utf8) ?? ""),
|
|
28
132
|
"price": p.price,
|
|
29
133
|
"subscription": p.subscription,
|
|
30
134
|
"type": p.type,
|
|
31
|
-
"currency": p.priceFormatStyle.currencyCode
|
|
135
|
+
"currency": p.priceFormatStyle.currencyCode,
|
|
136
|
+
"platform": "ios" // Add platform identifier
|
|
32
137
|
]
|
|
33
138
|
}
|
|
34
139
|
|
|
35
140
|
@available(iOS 15.0, *)
|
|
36
|
-
func
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
141
|
+
@Sendable func serialize(_ rs: Transaction.RefundRequestStatus?) -> String? {
|
|
142
|
+
guard let rs = rs else { return nil }
|
|
143
|
+
switch rs {
|
|
144
|
+
case .success: return "success"
|
|
145
|
+
case .userCancelled: return "userCancelled"
|
|
146
|
+
default:
|
|
147
|
+
return nil
|
|
148
|
+
}
|
|
44
149
|
}
|
|
45
150
|
|
|
46
151
|
@available(iOS 15.0, *)
|
|
@@ -70,22 +175,6 @@ func serializeRenewalInfo(_ renewalInfo: VerificationResult<Product.Subscription
|
|
|
70
175
|
}
|
|
71
176
|
}
|
|
72
177
|
|
|
73
|
-
@available(iOS 15.0, *)
|
|
74
|
-
func serialize(_ transaction: Transaction, _ result: VerificationResult<Transaction>) -> [String: Any?] {
|
|
75
|
-
return serializeTransaction(transaction)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
@available(iOS 15.0, *)
|
|
79
|
-
@Sendable func serialize(_ rs: Transaction.RefundRequestStatus?) -> String? {
|
|
80
|
-
guard let rs = rs else { return nil }
|
|
81
|
-
switch rs {
|
|
82
|
-
case .success: return "success"
|
|
83
|
-
case .userCancelled: return "userCancelled"
|
|
84
|
-
default:
|
|
85
|
-
return nil
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
178
|
@available(iOS 15.0, *)
|
|
90
179
|
public class ExpoIapModule: Module {
|
|
91
180
|
private var transactions: [String: Transaction] = [:]
|
|
@@ -103,12 +192,12 @@ public class ExpoIapModule: Module {
|
|
|
103
192
|
Events(IapEvent.PurchaseUpdated, IapEvent.PurchaseError, IapEvent.TransactionIapUpdated)
|
|
104
193
|
|
|
105
194
|
OnStartObserving {
|
|
106
|
-
hasListeners = true
|
|
195
|
+
self.hasListeners = true
|
|
107
196
|
self.addTransactionObserver()
|
|
108
197
|
}
|
|
109
198
|
|
|
110
199
|
OnStopObserving {
|
|
111
|
-
hasListeners = false
|
|
200
|
+
self.hasListeners = false
|
|
112
201
|
self.removeTransactionObserver()
|
|
113
202
|
}
|
|
114
203
|
|
|
@@ -310,7 +399,6 @@ public class ExpoIapModule: Module {
|
|
|
310
399
|
if let product = await productStore.getProduct(productID: sku) {
|
|
311
400
|
if let result = await product.currentEntitlement {
|
|
312
401
|
do {
|
|
313
|
-
// Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
|
|
314
402
|
let transaction = try self.checkVerified(result)
|
|
315
403
|
return serializeTransaction(transaction)
|
|
316
404
|
} catch StoreError.failedVerification {
|
|
@@ -334,7 +422,6 @@ public class ExpoIapModule: Module {
|
|
|
334
422
|
if let product = await productStore.getProduct(productID: sku) {
|
|
335
423
|
if let result = await product.latestTransaction {
|
|
336
424
|
do {
|
|
337
|
-
// Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
|
|
338
425
|
let transaction = try self.checkVerified(result)
|
|
339
426
|
return serializeTransaction(transaction)
|
|
340
427
|
} catch StoreError.failedVerification {
|
|
@@ -398,7 +485,6 @@ public class ExpoIapModule: Module {
|
|
|
398
485
|
Task {
|
|
399
486
|
for await result in Transaction.unfinished {
|
|
400
487
|
do {
|
|
401
|
-
// Check whether the transaction is verified. If it isn’t, catch `failedVerification` error.
|
|
402
488
|
let transaction = try self.checkVerified(result)
|
|
403
489
|
await transaction.finish()
|
|
404
490
|
self.transactions.removeValue(forKey: String(transaction.id))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-iap",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0-rc.2",
|
|
4
4
|
"description": "In App Purchase module in Expo",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"homepage": "https://github.com/hyochan#readme",
|
|
39
39
|
"dependencies": {},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@types/react": "
|
|
42
|
-
"expo-module-scripts": "^3.5.2",
|
|
43
|
-
"expo-modules-core": "^1.12.19",
|
|
41
|
+
"@types/react": "~18.2.79",
|
|
44
42
|
"eslint": "8.57.0",
|
|
45
43
|
"eslint-config-expo": "^7.1.2",
|
|
46
44
|
"eslint-config-prettier": "^9.1.0",
|
|
47
|
-
"eslint-plugin-prettier": "^5.1.3"
|
|
45
|
+
"eslint-plugin-prettier": "^5.1.3",
|
|
46
|
+
"expo-module-scripts": "^3.5.2",
|
|
47
|
+
"expo-modules-core": "~1.12.26"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"expo": "*",
|
package/plugin/build/withIAP.js
CHANGED
|
@@ -7,8 +7,13 @@ const pkg = require('../../package.json');
|
|
|
7
7
|
const addToBuildGradle = (newLine, anchor, offset, buildGradle) => {
|
|
8
8
|
const lines = buildGradle.split('\n');
|
|
9
9
|
const lineIndex = lines.findIndex((line) => line.match(anchor));
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
if (lineIndex === -1) {
|
|
11
|
+
console.warn('Anchor "ext" not found in build.gradle, appending to end');
|
|
12
|
+
lines.push(newLine);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
lines.splice(lineIndex + offset, 0, newLine);
|
|
16
|
+
}
|
|
12
17
|
return lines.join('\n');
|
|
13
18
|
};
|
|
14
19
|
const modifyProjectBuildGradle = (buildGradle) => {
|
|
@@ -24,14 +29,36 @@ const withIAPAndroid = (config) => {
|
|
|
24
29
|
config.modResults.contents = (0, exports.modifyProjectBuildGradle)(config.modResults.contents);
|
|
25
30
|
return config;
|
|
26
31
|
});
|
|
32
|
+
// Adding BILLING permission to AndroidManifest.xml
|
|
33
|
+
config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
|
|
34
|
+
console.log('Modifying AndroidManifest.xml...');
|
|
35
|
+
const manifest = config.modResults;
|
|
36
|
+
if (!manifest.manifest['uses-permission']) {
|
|
37
|
+
manifest.manifest['uses-permission'] = [];
|
|
38
|
+
}
|
|
39
|
+
const permissions = manifest.manifest['uses-permission'];
|
|
40
|
+
const billingPermission = {
|
|
41
|
+
$: { 'android:name': 'com.android.vending.BILLING' },
|
|
42
|
+
};
|
|
43
|
+
if (!permissions.some((perm) => perm.$['android:name'] === 'com.android.vending.BILLING')) {
|
|
44
|
+
permissions.push(billingPermission);
|
|
45
|
+
console.log('Added com.android.vending.BILLING to permissions');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.log('com.android.vending.BILLING already exists in manifest');
|
|
49
|
+
}
|
|
50
|
+
return config;
|
|
51
|
+
});
|
|
27
52
|
return config;
|
|
28
53
|
};
|
|
29
54
|
const withIAP = (config, props) => {
|
|
30
55
|
try {
|
|
56
|
+
console.log('Applying expo-iap plugin...');
|
|
31
57
|
config = withIAPAndroid(config);
|
|
32
58
|
}
|
|
33
59
|
catch (error) {
|
|
34
60
|
config_plugins_1.WarningAggregator.addWarningAndroid('expo-iap', `There was a problem configuring expo-iap in your native Android project: ${error}`);
|
|
61
|
+
console.error('Error in expo-iap plugin:', error);
|
|
35
62
|
}
|
|
36
63
|
return config;
|
|
37
64
|
};
|
package/plugin/src/withIAP.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
WarningAggregator,
|
|
3
|
+
withAndroidManifest,
|
|
4
|
+
withProjectBuildGradle,
|
|
5
|
+
} from 'expo/config-plugins';
|
|
2
6
|
import {ConfigPlugin, createRunOncePlugin} from 'expo/config-plugins';
|
|
3
7
|
|
|
4
8
|
const pkg = require('../../package.json');
|
|
@@ -11,8 +15,12 @@ const addToBuildGradle = (
|
|
|
11
15
|
) => {
|
|
12
16
|
const lines = buildGradle.split('\n');
|
|
13
17
|
const lineIndex = lines.findIndex((line) => line.match(anchor));
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
if (lineIndex === -1) {
|
|
19
|
+
console.warn('Anchor "ext" not found in build.gradle, appending to end');
|
|
20
|
+
lines.push(newLine);
|
|
21
|
+
} else {
|
|
22
|
+
lines.splice(lineIndex + offset, 0, newLine);
|
|
23
|
+
}
|
|
16
24
|
return lines.join('\n');
|
|
17
25
|
};
|
|
18
26
|
|
|
@@ -31,6 +39,34 @@ const withIAPAndroid: ConfigPlugin = (config) => {
|
|
|
31
39
|
);
|
|
32
40
|
return config;
|
|
33
41
|
});
|
|
42
|
+
|
|
43
|
+
// Adding BILLING permission to AndroidManifest.xml
|
|
44
|
+
config = withAndroidManifest(config, (config) => {
|
|
45
|
+
console.log('Modifying AndroidManifest.xml...');
|
|
46
|
+
const manifest = config.modResults;
|
|
47
|
+
|
|
48
|
+
if (!manifest.manifest['uses-permission']) {
|
|
49
|
+
manifest.manifest['uses-permission'] = [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const permissions = manifest.manifest['uses-permission'];
|
|
53
|
+
const billingPermission = {
|
|
54
|
+
$: {'android:name': 'com.android.vending.BILLING'},
|
|
55
|
+
};
|
|
56
|
+
if (
|
|
57
|
+
!permissions.some(
|
|
58
|
+
(perm: any) => perm.$['android:name'] === 'com.android.vending.BILLING',
|
|
59
|
+
)
|
|
60
|
+
) {
|
|
61
|
+
permissions.push(billingPermission);
|
|
62
|
+
console.log('Added com.android.vending.BILLING to permissions');
|
|
63
|
+
} else {
|
|
64
|
+
console.log('com.android.vending.BILLING already exists in manifest');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return config;
|
|
68
|
+
});
|
|
69
|
+
|
|
34
70
|
return config;
|
|
35
71
|
};
|
|
36
72
|
|
|
@@ -38,14 +74,15 @@ interface Props {}
|
|
|
38
74
|
|
|
39
75
|
const withIAP: ConfigPlugin<Props | undefined> = (config, props) => {
|
|
40
76
|
try {
|
|
77
|
+
console.log('Applying expo-iap plugin...');
|
|
41
78
|
config = withIAPAndroid(config);
|
|
42
79
|
} catch (error) {
|
|
43
80
|
WarningAggregator.addWarningAndroid(
|
|
44
81
|
'expo-iap',
|
|
45
82
|
`There was a problem configuring expo-iap in your native Android project: ${error}`,
|
|
46
83
|
);
|
|
84
|
+
console.error('Error in expo-iap plugin:', error);
|
|
47
85
|
}
|
|
48
|
-
|
|
49
86
|
return config;
|
|
50
87
|
};
|
|
51
88
|
|
package/src/ExpoIap.types.ts
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ProductAndroid,
|
|
3
|
+
ProductPurchaseAndroid,
|
|
3
4
|
RequestPurchaseAndroidProps,
|
|
4
5
|
RequestSubscriptionAndroidProps,
|
|
5
6
|
SubscriptionProductAndroid,
|
|
6
7
|
} from './types/ExpoIapAndroid.types';
|
|
7
8
|
import {
|
|
8
9
|
ProductIos,
|
|
10
|
+
ProductPurchaseIos,
|
|
9
11
|
RequestPurchaseIosProps,
|
|
10
12
|
RequestSubscriptionIosProps,
|
|
11
13
|
SubscriptionProductIos,
|
|
12
14
|
} from './types/ExpoIapIos.types';
|
|
15
|
+
|
|
13
16
|
export type ChangeEventPayload = {
|
|
14
17
|
value: string;
|
|
15
18
|
};
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Base product type with common properties shared between iOS and Android
|
|
22
|
+
*/
|
|
23
|
+
export type ProductBase = {
|
|
24
|
+
id: string;
|
|
25
|
+
title: string;
|
|
26
|
+
description: string;
|
|
27
|
+
type: ProductType;
|
|
28
|
+
displayName?: string;
|
|
29
|
+
displayPrice?: string;
|
|
30
|
+
price?: number;
|
|
31
|
+
currency?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Define literal platform types for better type discrimination
|
|
35
|
+
export type IosPlatform = {platform: 'ios'};
|
|
36
|
+
export type AndroidPlatform = {platform: 'android'};
|
|
37
|
+
|
|
18
38
|
export enum ProductType {
|
|
19
39
|
InAppPurchase = 'inapp',
|
|
20
40
|
Subscription = 'subs',
|
|
21
41
|
}
|
|
22
42
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export type RequestPurchaseProps =
|
|
28
|
-
| RequestPurchaseIosProps
|
|
29
|
-
| RequestPurchaseAndroidProps;
|
|
30
|
-
|
|
31
|
-
enum PurchaseStateAndroid {
|
|
32
|
-
UNSPECIFIED_STATE = 0,
|
|
33
|
-
PURCHASED = 1,
|
|
34
|
-
PENDING = 2,
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type ProductPurchase = {
|
|
38
|
-
productId: string;
|
|
43
|
+
// Common base purchase type
|
|
44
|
+
export type PurchaseBase = {
|
|
45
|
+
id: string;
|
|
39
46
|
transactionId?: string;
|
|
40
47
|
transactionDate: number;
|
|
41
48
|
transactionReceipt: string;
|
|
42
49
|
purchaseToken?: string;
|
|
43
|
-
//iOS
|
|
44
|
-
quantityIOS?: number;
|
|
45
|
-
originalTransactionDateIOS?: number;
|
|
46
|
-
originalTransactionIdentifierIOS?: string;
|
|
47
|
-
verificationResultIOS?: string;
|
|
48
|
-
appAccountToken?: string;
|
|
49
|
-
//Android
|
|
50
|
-
productIds?: string[];
|
|
51
|
-
dataAndroid?: string;
|
|
52
|
-
signatureAndroid?: string;
|
|
53
|
-
autoRenewingAndroid?: boolean;
|
|
54
|
-
purchaseStateAndroid?: PurchaseStateAndroid;
|
|
55
|
-
isAcknowledgedAndroid?: boolean;
|
|
56
|
-
packageNameAndroid?: string;
|
|
57
|
-
developerPayloadAndroid?: string;
|
|
58
|
-
obfuscatedAccountIdAndroid?: string;
|
|
59
|
-
obfuscatedProfileIdAndroid?: string;
|
|
60
50
|
};
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
|
52
|
+
// Union type for platform-specific product types with proper discriminators
|
|
53
|
+
export type Product =
|
|
54
|
+
| (ProductAndroid & AndroidPlatform)
|
|
55
|
+
| (ProductIos & IosPlatform);
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
// Union type for platform-specific purchase types with proper discriminators
|
|
58
|
+
export type ProductPurchase =
|
|
59
|
+
| (ProductPurchaseAndroid & AndroidPlatform)
|
|
60
|
+
| (ProductPurchaseIos & IosPlatform);
|
|
70
61
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
verificationResultIOS?: string;
|
|
76
|
-
transactionReasonIOS?: TransactionReason | string;
|
|
77
|
-
} & ProductPurchase;
|
|
62
|
+
// Union type for platform-specific subscription purchase types with proper discriminators
|
|
63
|
+
export type SubscriptionPurchase =
|
|
64
|
+
| (ProductPurchaseAndroid & AndroidPlatform & { autoRenewingAndroid: boolean })
|
|
65
|
+
| (ProductPurchaseIos & IosPlatform);
|
|
78
66
|
|
|
79
67
|
export type Purchase = ProductPurchase | SubscriptionPurchase;
|
|
80
68
|
|
|
69
|
+
export type RequestPurchaseProps =
|
|
70
|
+
| RequestPurchaseIosProps
|
|
71
|
+
| RequestPurchaseAndroidProps;
|
|
72
|
+
|
|
73
|
+
export type SubscriptionProduct =
|
|
74
|
+
| (SubscriptionProductAndroid & AndroidPlatform)
|
|
75
|
+
| (SubscriptionProductIos & IosPlatform);
|
|
76
|
+
|
|
77
|
+
export type RequestSubscriptionProps =
|
|
78
|
+
| RequestSubscriptionAndroidProps
|
|
79
|
+
| RequestSubscriptionIosProps;
|
|
80
|
+
|
|
81
81
|
export type PurchaseResult = {
|
|
82
82
|
responseCode?: number;
|
|
83
83
|
debugMessage?: string;
|