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.
Files changed (42) hide show
  1. package/README.md +8 -27
  2. package/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +10 -9
  3. package/build/ExpoIap.types.d.ts +31 -40
  4. package/build/ExpoIap.types.d.ts.map +1 -1
  5. package/build/ExpoIap.types.js +0 -11
  6. package/build/ExpoIap.types.js.map +1 -1
  7. package/build/index.d.ts +2 -4
  8. package/build/index.d.ts.map +1 -1
  9. package/build/index.js +32 -46
  10. package/build/index.js.map +1 -1
  11. package/build/modules/android.d.ts +12 -4
  12. package/build/modules/android.d.ts.map +1 -1
  13. package/build/modules/android.js +8 -4
  14. package/build/modules/android.js.map +1 -1
  15. package/build/modules/ios.d.ts +15 -7
  16. package/build/modules/ios.d.ts.map +1 -1
  17. package/build/modules/ios.js +9 -5
  18. package/build/modules/ios.js.map +1 -1
  19. package/build/types/ExpoIapAndroid.types.d.ts +31 -31
  20. package/build/types/ExpoIapAndroid.types.d.ts.map +1 -1
  21. package/build/types/ExpoIapAndroid.types.js +6 -0
  22. package/build/types/ExpoIapAndroid.types.js.map +1 -1
  23. package/build/types/ExpoIapIos.types.d.ts +31 -34
  24. package/build/types/ExpoIapIos.types.d.ts.map +1 -1
  25. package/build/types/ExpoIapIos.types.js.map +1 -1
  26. package/build/useIap.d.ts +1 -2
  27. package/build/useIap.d.ts.map +1 -1
  28. package/build/useIap.js +3 -3
  29. package/build/useIap.js.map +1 -1
  30. package/bun.lockb +0 -0
  31. package/iap.md +161 -0
  32. package/ios/ExpoIapModule.swift +116 -30
  33. package/package.json +5 -5
  34. package/plugin/build/withIAP.js +29 -2
  35. package/plugin/src/withIAP.ts +41 -4
  36. package/src/ExpoIap.types.ts +48 -48
  37. package/src/index.ts +48 -96
  38. package/src/modules/android.ts +16 -12
  39. package/src/modules/ios.ts +20 -17
  40. package/src/types/ExpoIapAndroid.types.ts +37 -33
  41. package/src/types/ExpoIapIos.types.ts +36 -40
  42. 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.
@@ -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 serializeTransaction(_ transaction: Transaction) -> [String: Any?] {
37
- return [
38
- "id": transaction.id,
39
- "productID": transaction.productID,
40
- "purchaseDate": transaction.purchaseDate,
41
- "expirationDate": transaction.expirationDate,
42
- "originalID": transaction.originalID
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.1.0",
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": "^18.0.25",
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": "*",
@@ -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
- // add after given line
11
- lines.splice(lineIndex + offset, 0, newLine);
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
  };
@@ -1,4 +1,8 @@
1
- import {WarningAggregator, withProjectBuildGradle} from 'expo/config-plugins';
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
- // add after given line
15
- lines.splice(lineIndex + offset, 0, newLine);
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
 
@@ -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
- export type Product = ProductAndroid | ProductIos;
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
- export type SubscriptionProduct =
24
- | SubscriptionProductAndroid
25
- | SubscriptionProductIos;
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
- export type RequestSubscriptionProps =
63
- | RequestSubscriptionAndroidProps
64
- | RequestSubscriptionIosProps;
52
+ // Union type for platform-specific product types with proper discriminators
53
+ export type Product =
54
+ | (ProductAndroid & AndroidPlatform)
55
+ | (ProductIos & IosPlatform);
65
56
 
66
- enum TransactionReason {
67
- PURCHASE = 'PURCHASE',
68
- RENEWAL = 'RENEWAL',
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
- export type SubscriptionPurchase = {
72
- autoRenewingAndroid?: boolean;
73
- originalTransactionDateIOS?: number;
74
- originalTransactionIdentifierIOS?: string;
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;