expo-iap 4.3.2 → 4.3.4
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/android/src/main/java/expo/modules/iap/ExpoIapModule.kt +2 -1
- package/ios/onside/OnsideIapModule.swift +53 -24
- package/openiap-versions.json +1 -1
- package/package.json +1 -1
- package/plugin/build/withLocalOpenIAP.d.ts +2 -0
- package/plugin/build/withLocalOpenIAP.js +71 -6
- package/plugin/src/withLocalOpenIAP.ts +101 -6
|
@@ -3,6 +3,7 @@ package expo.modules.iap
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import dev.hyo.openiap.AndroidSubscriptionOfferInput
|
|
5
5
|
import dev.hyo.openiap.DeepLinkOptions
|
|
6
|
+
import dev.hyo.openiap.FetchProductsResultAll
|
|
6
7
|
import dev.hyo.openiap.FetchProductsResultProducts
|
|
7
8
|
import dev.hyo.openiap.FetchProductsResultSubscriptions
|
|
8
9
|
import dev.hyo.openiap.InitConnectionConfig
|
|
@@ -183,9 +184,9 @@ class ExpoIapModule : Module() {
|
|
|
183
184
|
val result = openIap.fetchProducts(request)
|
|
184
185
|
val payload =
|
|
185
186
|
when (result) {
|
|
187
|
+
is FetchProductsResultAll -> result.value.orEmpty().map { it.toJson() }
|
|
186
188
|
is FetchProductsResultProducts -> result.value.orEmpty().map { it.toJson() }
|
|
187
189
|
is FetchProductsResultSubscriptions -> result.value.orEmpty().map { it.toJson() }
|
|
188
|
-
else -> emptyList<Map<String, Any?>>()
|
|
189
190
|
}
|
|
190
191
|
ExpoIapLog.result("fetchProducts", payload)
|
|
191
192
|
promise.resolve(payload)
|
|
@@ -50,6 +50,7 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
50
50
|
private let transactionObserver = OnsideTransactionObserverBridge()
|
|
51
51
|
private let productFetcher = OnsideProductFetcher()
|
|
52
52
|
private var productCache: [String: OnsideProduct] = [:]
|
|
53
|
+
private var transactionDateCache: [String: Date] = [:]
|
|
53
54
|
|
|
54
55
|
nonisolated public func definition() -> ModuleDefinition {
|
|
55
56
|
Name("ExpoIapOnside")
|
|
@@ -113,7 +114,7 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
113
114
|
|
|
114
115
|
// Check if Onside Store is installed
|
|
115
116
|
if let onsideURL = URL(string: "onside://"),
|
|
116
|
-
UIApplication.shared.canOpenURL(onsideURL) {
|
|
117
|
+
await MainActor.run(body: { UIApplication.shared.canOpenURL(onsideURL) }) {
|
|
117
118
|
#if DEBUG
|
|
118
119
|
print("[ExpoIapOnsideModule] ✅ Onside Store app is installed")
|
|
119
120
|
#endif
|
|
@@ -225,16 +226,15 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
225
226
|
let productId = purchasePayload["productId"] as? String
|
|
226
227
|
let txId = purchasePayload["transactionId"] as? String
|
|
227
228
|
|
|
228
|
-
let queue = await Onside.defaultPaymentQueue()
|
|
229
|
-
|
|
230
229
|
let transaction: OnsidePaymentTransaction? = await MainActor.run {
|
|
230
|
+
let transactions = Onside.defaultPaymentQueue().transactions
|
|
231
231
|
if let txId, !txId.isEmpty {
|
|
232
|
-
return
|
|
232
|
+
return transactions.first(where: { $0.transactionIdentifier == txId })
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
// 2) fallback: if txId is not available yet — search by productId (less reliable!)
|
|
236
236
|
if let productId, !productId.isEmpty {
|
|
237
|
-
return
|
|
237
|
+
return transactions.first(where: {
|
|
238
238
|
$0.payment.product.productIdentifier == productId
|
|
239
239
|
&& ($0.transactionState == .purchased || $0.transactionState == .restored)
|
|
240
240
|
})
|
|
@@ -247,7 +247,9 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
247
247
|
throw OnsideBridgeError.transactionNotFound(txId ?? productId ?? "")
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
await
|
|
250
|
+
await MainActor.run {
|
|
251
|
+
Onside.defaultPaymentQueue().finishTransaction(transaction)
|
|
252
|
+
}
|
|
251
253
|
ExpoIapLog.result("finishTransactionOnside", value: true)
|
|
252
254
|
return true
|
|
253
255
|
}
|
|
@@ -298,14 +300,21 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
298
300
|
]
|
|
299
301
|
)
|
|
300
302
|
try await ensureObserverRegistered()
|
|
301
|
-
let
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
303
|
+
let payload: [[String: Any]] = try await MainActor.run {
|
|
304
|
+
let items = try Onside.defaultPaymentQueue().transactions.compactMap { transaction -> [String: Any]? in
|
|
305
|
+
switch transaction.transactionState {
|
|
306
|
+
case .purchased, .restored:
|
|
307
|
+
return try serialize(transaction: transaction)
|
|
308
|
+
default:
|
|
309
|
+
return nil
|
|
310
|
+
}
|
|
308
311
|
}
|
|
312
|
+
if alsoPublish {
|
|
313
|
+
items.forEach {
|
|
314
|
+
sendEvent(OnsideEvent.purchaseUpdated.rawValue, $0)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return items
|
|
309
318
|
}
|
|
310
319
|
ExpoIapLog.result("getAvailableItemsOnside", value: payload)
|
|
311
320
|
return payload
|
|
@@ -323,7 +332,7 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
323
332
|
private func getOnsideStorefront() async throws -> String {
|
|
324
333
|
ExpoIapLog.payload("getStorefrontOnside", payload: nil)
|
|
325
334
|
try await ensureObserverRegistered()
|
|
326
|
-
let storefront =
|
|
335
|
+
let storefront = Onside.defaultPaymentQueue().storefront?.countryCode ?? ""
|
|
327
336
|
ExpoIapLog.result("getStorefrontOnside", value: storefront)
|
|
328
337
|
return storefront
|
|
329
338
|
}
|
|
@@ -385,6 +394,7 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
385
394
|
let cont = restoreContinuation
|
|
386
395
|
restoreContinuation = nil
|
|
387
396
|
cont?.resume(returning: false)
|
|
397
|
+
transactionDateCache.removeAll()
|
|
388
398
|
}
|
|
389
399
|
|
|
390
400
|
private func handle(transaction: OnsidePaymentTransaction) {
|
|
@@ -425,11 +435,11 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
425
435
|
dictionary["displayNameIOS"] = product.localizedTitle
|
|
426
436
|
let formatter = NumberFormatter()
|
|
427
437
|
formatter.numberStyle = .currency
|
|
428
|
-
formatter.currencyCode = product.price.currencyCode
|
|
429
|
-
let priceNumber = NSDecimalNumber(
|
|
438
|
+
formatter.currencyCode = product.price.currencyCode
|
|
439
|
+
let priceNumber = NSDecimalNumber(string: String(product.price.value))
|
|
430
440
|
let formattedPrice = formatter.string(from: priceNumber) ?? "\(product.price.value)"
|
|
431
441
|
dictionary["displayPrice"] = formattedPrice
|
|
432
|
-
dictionary["currency"] = product.price.currencyCode
|
|
442
|
+
dictionary["currency"] = product.price.currencyCode
|
|
433
443
|
dictionary["price"] = priceNumber
|
|
434
444
|
dictionary["type"] = "in-app"
|
|
435
445
|
dictionary["typeIOS"] = "non-consumable"
|
|
@@ -450,15 +460,15 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
450
460
|
dictionary["quantity"] = 1
|
|
451
461
|
dictionary["isAutoRenewing"] = false
|
|
452
462
|
dictionary["purchaseState"] = mapPurchaseState(transaction.transactionState)
|
|
453
|
-
let txDate = transaction
|
|
463
|
+
let txDate = date(for: transaction)
|
|
454
464
|
dictionary["transactionDate"] = Int(txDate.timeIntervalSince1970 * 1000)
|
|
455
|
-
dictionary["currencyCodeIOS"] = product.price.currencyCode
|
|
465
|
+
dictionary["currencyCodeIOS"] = product.price.currencyCode
|
|
456
466
|
let currencyFormatter = NumberFormatter()
|
|
457
467
|
currencyFormatter.numberStyle = .currency
|
|
458
|
-
currencyFormatter.currencyCode = product.price.currencyCode
|
|
468
|
+
currencyFormatter.currencyCode = product.price.currencyCode
|
|
459
469
|
dictionary["currencySymbolIOS"] = currencyFormatter.currencySymbol ?? ""
|
|
460
470
|
|
|
461
|
-
dictionary["storefrontCountryCodeIOS"] = transaction.storefront.countryCode
|
|
471
|
+
dictionary["storefrontCountryCodeIOS"] = transaction.storefront.countryCode
|
|
462
472
|
dictionary["purchaseToken"] = nil
|
|
463
473
|
dictionary["environmentIOS"] = transaction.storefront.id
|
|
464
474
|
if let error = transaction.error {
|
|
@@ -467,12 +477,30 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
467
477
|
return sanitize(dictionary)
|
|
468
478
|
}
|
|
469
479
|
|
|
480
|
+
private func date(for transaction: OnsidePaymentTransaction) -> Date {
|
|
481
|
+
guard let key = transaction.transactionIdentifier ?? transaction.originalTransactionIdentifier,
|
|
482
|
+
!key.isEmpty
|
|
483
|
+
else {
|
|
484
|
+
return Date()
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if let cachedDate = transactionDateCache[key] {
|
|
488
|
+
return cachedDate
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// OnsideKit currently exposes no purchase date on the transaction, so
|
|
492
|
+
// keep the first observed timestamp stable for repeated serializations.
|
|
493
|
+
let observedDate = Date()
|
|
494
|
+
transactionDateCache[key] = observedDate
|
|
495
|
+
return observedDate
|
|
496
|
+
}
|
|
497
|
+
|
|
470
498
|
// Build a JSON string from known product fields (no Encodable conformance required)
|
|
471
499
|
private func makeProductJSONRepresentation(from product: OnsideProduct) throws -> String {
|
|
472
500
|
let priceFormatter = NumberFormatter()
|
|
473
501
|
priceFormatter.numberStyle = .currency
|
|
474
|
-
priceFormatter.currencyCode = product.price.currencyCode
|
|
475
|
-
let priceNumber = NSDecimalNumber(
|
|
502
|
+
priceFormatter.currencyCode = product.price.currencyCode
|
|
503
|
+
let priceNumber = NSDecimalNumber(string: String(product.price.value))
|
|
476
504
|
let formattedPrice = priceFormatter.string(from: priceNumber) ?? "\(product.price.value)"
|
|
477
505
|
let jsonObject: [String: Any] = [
|
|
478
506
|
"id": product.productIdentifier,
|
|
@@ -480,7 +508,7 @@ public final class ExpoIapOnsideModule: Module {
|
|
|
480
508
|
"description": product.localizedDescription,
|
|
481
509
|
"price": [
|
|
482
510
|
"value": priceNumber,
|
|
483
|
-
"currencyCode": product.price.currencyCode
|
|
511
|
+
"currencyCode": product.price.currencyCode,
|
|
484
512
|
"formatted": formattedPrice,
|
|
485
513
|
],
|
|
486
514
|
"isFamilyShareable": false,
|
|
@@ -655,6 +683,7 @@ private final class OnsideProductFetcher: NSObject, OnsideProductsRequestDelegat
|
|
|
655
683
|
}
|
|
656
684
|
}
|
|
657
685
|
|
|
686
|
+
@MainActor
|
|
658
687
|
private func cleanup() {
|
|
659
688
|
request?.delegate = nil
|
|
660
689
|
request?.stop()
|
package/openiap-versions.json
CHANGED
package/package.json
CHANGED
|
@@ -8,6 +8,8 @@ type LocalPathOption = string | {
|
|
|
8
8
|
ios?: string;
|
|
9
9
|
android?: string;
|
|
10
10
|
};
|
|
11
|
+
type GradleLanguage = 'groovy' | 'kotlin';
|
|
12
|
+
export declare const ensureLocalOpenIapFlavorStrategy: (contents: string, flavor: "play" | "horizon", language?: GradleLanguage) => string;
|
|
11
13
|
declare const withLocalOpenIAP: ConfigPlugin<{
|
|
12
14
|
localPath?: LocalPathOption;
|
|
13
15
|
iosAlternativeBilling?: IOSAlternativeBillingConfig;
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ensureLocalOpenIapFlavorStrategy = void 0;
|
|
36
37
|
const config_plugins_1 = require("expo/config-plugins");
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
@@ -72,6 +73,42 @@ const logOnce = (() => {
|
|
|
72
73
|
}
|
|
73
74
|
};
|
|
74
75
|
})();
|
|
76
|
+
const LOCAL_OPENIAP_FLAVOR_BLOCK_START = '// Added by expo-iap (local openiap-google flavor selection)';
|
|
77
|
+
const LOCAL_OPENIAP_FLAVOR_BLOCK_END = '// End expo-iap local openiap-google flavor selection';
|
|
78
|
+
const normalizeGradleLanguage = (language) => language === 'kotlin' ? 'kotlin' : 'groovy';
|
|
79
|
+
const ensureLocalOpenIapFlavorStrategy = (contents, flavor, language = 'groovy') => {
|
|
80
|
+
const existingBlockPattern = new RegExp(`\\n?${escapeRegExp(LOCAL_OPENIAP_FLAVOR_BLOCK_START)}[\\s\\S]*?${escapeRegExp(LOCAL_OPENIAP_FLAVOR_BLOCK_END)}\\n?`, 'gm');
|
|
81
|
+
const cleaned = contents
|
|
82
|
+
.replace(existingBlockPattern, '\n')
|
|
83
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
84
|
+
.trimEnd();
|
|
85
|
+
const strategyBlock = language === 'kotlin'
|
|
86
|
+
? `subprojects {
|
|
87
|
+
plugins.withId("com.android.library") {
|
|
88
|
+
extensions.configure<com.android.build.gradle.LibraryExtension>("android") {
|
|
89
|
+
defaultConfig {
|
|
90
|
+
missingDimensionStrategy("platform", "${flavor}")
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}`
|
|
95
|
+
: `subprojects { subproject ->
|
|
96
|
+
subproject.plugins.withId("com.android.library") {
|
|
97
|
+
subproject.android {
|
|
98
|
+
defaultConfig {
|
|
99
|
+
missingDimensionStrategy "platform", "${flavor}"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}`;
|
|
104
|
+
return `${cleaned}
|
|
105
|
+
|
|
106
|
+
${LOCAL_OPENIAP_FLAVOR_BLOCK_START}
|
|
107
|
+
${strategyBlock}
|
|
108
|
+
${LOCAL_OPENIAP_FLAVOR_BLOCK_END}
|
|
109
|
+
`;
|
|
110
|
+
};
|
|
111
|
+
exports.ensureLocalOpenIapFlavorStrategy = ensureLocalOpenIapFlavorStrategy;
|
|
75
112
|
const withLocalOpenIAP = (config, props) => {
|
|
76
113
|
// Import and apply iOS alternative billing configuration if provided
|
|
77
114
|
if (props?.iosAlternativeBilling) {
|
|
@@ -162,9 +199,15 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
162
199
|
.replace(/\\/g, '/');
|
|
163
200
|
// 1) settings.gradle: include and map projectDir
|
|
164
201
|
const settings = config.modResults;
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
202
|
+
const settingsLanguage = normalizeGradleLanguage(settings.language);
|
|
203
|
+
const includeLine = settingsLanguage === 'kotlin'
|
|
204
|
+
? 'include(":openiap-google")'
|
|
205
|
+
: "include ':openiap-google'";
|
|
206
|
+
const projectDirLine = settingsLanguage === 'kotlin'
|
|
207
|
+
? `project(":openiap-google").projectDir = File(settingsDir, "${relativeAndroidModulePath}")`
|
|
208
|
+
: `project(':openiap-google').projectDir = new File(settingsDir, '${relativeAndroidModulePath}')`;
|
|
209
|
+
const includePattern = /include\s*(?:\(\s*)?["']:openiap-google["']\s*\)?/;
|
|
210
|
+
const projectDirPattern = /^\s*project\(["']:openiap-google["']\)\.projectDir\s*=.*$/gm;
|
|
168
211
|
let contents = settings.contents ?? '';
|
|
169
212
|
// Ensure pluginManagement has plugin mappings required by the included module
|
|
170
213
|
const injectPluginManagement = () => {
|
|
@@ -208,7 +251,7 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
208
251
|
}
|
|
209
252
|
};
|
|
210
253
|
injectPluginManagement();
|
|
211
|
-
if (!
|
|
254
|
+
if (!includePattern.test(contents))
|
|
212
255
|
contents += `\n${includeLine}\n`;
|
|
213
256
|
if (projectDirPattern.test(contents)) {
|
|
214
257
|
contents = contents.replace(projectDirPattern, projectDirLine);
|
|
@@ -232,9 +275,14 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
232
275
|
return config;
|
|
233
276
|
}
|
|
234
277
|
const gradle = config.modResults;
|
|
235
|
-
const
|
|
278
|
+
const appLanguage = normalizeGradleLanguage(gradle.language);
|
|
279
|
+
const dependencyLine = appLanguage === 'kotlin'
|
|
280
|
+
? ` implementation(project(":openiap-google"))`
|
|
281
|
+
: ` implementation project(':openiap-google')`;
|
|
236
282
|
const flavor = props?.isHorizonEnabled ? 'horizon' : 'play';
|
|
237
|
-
const strategyLine =
|
|
283
|
+
const strategyLine = appLanguage === 'kotlin'
|
|
284
|
+
? ` missingDimensionStrategy("platform", "${flavor}")`
|
|
285
|
+
: ` missingDimensionStrategy "platform", "${flavor}"`;
|
|
238
286
|
let contents = gradle.contents;
|
|
239
287
|
// Remove Maven deps (both openiap-google and openiap-google-horizon)
|
|
240
288
|
// to avoid duplicate classes with local module
|
|
@@ -273,6 +321,23 @@ const withLocalOpenIAP = (config, props) => {
|
|
|
273
321
|
gradle.contents = contents;
|
|
274
322
|
return config;
|
|
275
323
|
});
|
|
324
|
+
// 2b) project build.gradle: Expo autolinked library modules can consume the
|
|
325
|
+
// local flavored OpenIAP module transitively, so give them the same default.
|
|
326
|
+
config = (0, config_plugins_1.withProjectBuildGradle)(config, (config) => {
|
|
327
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
328
|
+
const raw = props?.localPath;
|
|
329
|
+
const androidInput = typeof raw === 'string' ? undefined : raw?.android;
|
|
330
|
+
const androidModulePath = resolveAndroidModulePath(androidInput) ||
|
|
331
|
+
resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) ||
|
|
332
|
+
null;
|
|
333
|
+
if (!androidModulePath || !fs.existsSync(androidModulePath)) {
|
|
334
|
+
return config;
|
|
335
|
+
}
|
|
336
|
+
const flavor = props?.isHorizonEnabled ? 'horizon' : 'play';
|
|
337
|
+
config.modResults.contents = (0, exports.ensureLocalOpenIapFlavorStrategy)(config.modResults.contents, flavor, normalizeGradleLanguage(config.modResults.language));
|
|
338
|
+
logOnce(`🛠️ expo-iap: Added local OpenIAP flavor strategy for ${flavor}`);
|
|
339
|
+
return config;
|
|
340
|
+
});
|
|
276
341
|
// 3) Set horizonEnabled in gradle.properties
|
|
277
342
|
config = (0, config_plugins_1.withDangerousMod)(config, [
|
|
278
343
|
'android',
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
withDangerousMod,
|
|
4
4
|
withSettingsGradle,
|
|
5
5
|
withAppBuildGradle,
|
|
6
|
+
withProjectBuildGradle,
|
|
6
7
|
} from 'expo/config-plugins';
|
|
7
8
|
import * as fs from 'fs';
|
|
8
9
|
import * as path from 'path';
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
* This is only for local development with openiap-apple library
|
|
17
18
|
*/
|
|
18
19
|
type LocalPathOption = string | {ios?: string; android?: string};
|
|
20
|
+
type GradleLanguage = 'groovy' | 'kotlin';
|
|
19
21
|
|
|
20
22
|
interface AndroidGradlePluginVersions {
|
|
21
23
|
kotlin: string;
|
|
@@ -86,6 +88,59 @@ const logOnce = (() => {
|
|
|
86
88
|
};
|
|
87
89
|
})();
|
|
88
90
|
|
|
91
|
+
const LOCAL_OPENIAP_FLAVOR_BLOCK_START =
|
|
92
|
+
'// Added by expo-iap (local openiap-google flavor selection)';
|
|
93
|
+
const LOCAL_OPENIAP_FLAVOR_BLOCK_END =
|
|
94
|
+
'// End expo-iap local openiap-google flavor selection';
|
|
95
|
+
|
|
96
|
+
const normalizeGradleLanguage = (language?: string): GradleLanguage =>
|
|
97
|
+
language === 'kotlin' ? 'kotlin' : 'groovy';
|
|
98
|
+
|
|
99
|
+
export const ensureLocalOpenIapFlavorStrategy = (
|
|
100
|
+
contents: string,
|
|
101
|
+
flavor: 'play' | 'horizon',
|
|
102
|
+
language: GradleLanguage = 'groovy',
|
|
103
|
+
): string => {
|
|
104
|
+
const existingBlockPattern = new RegExp(
|
|
105
|
+
`\\n?${escapeRegExp(
|
|
106
|
+
LOCAL_OPENIAP_FLAVOR_BLOCK_START,
|
|
107
|
+
)}[\\s\\S]*?${escapeRegExp(LOCAL_OPENIAP_FLAVOR_BLOCK_END)}\\n?`,
|
|
108
|
+
'gm',
|
|
109
|
+
);
|
|
110
|
+
const cleaned = contents
|
|
111
|
+
.replace(existingBlockPattern, '\n')
|
|
112
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
113
|
+
.trimEnd();
|
|
114
|
+
|
|
115
|
+
const strategyBlock =
|
|
116
|
+
language === 'kotlin'
|
|
117
|
+
? `subprojects {
|
|
118
|
+
plugins.withId("com.android.library") {
|
|
119
|
+
extensions.configure<com.android.build.gradle.LibraryExtension>("android") {
|
|
120
|
+
defaultConfig {
|
|
121
|
+
missingDimensionStrategy("platform", "${flavor}")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}`
|
|
126
|
+
: `subprojects { subproject ->
|
|
127
|
+
subproject.plugins.withId("com.android.library") {
|
|
128
|
+
subproject.android {
|
|
129
|
+
defaultConfig {
|
|
130
|
+
missingDimensionStrategy "platform", "${flavor}"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}`;
|
|
135
|
+
|
|
136
|
+
return `${cleaned}
|
|
137
|
+
|
|
138
|
+
${LOCAL_OPENIAP_FLAVOR_BLOCK_START}
|
|
139
|
+
${strategyBlock}
|
|
140
|
+
${LOCAL_OPENIAP_FLAVOR_BLOCK_END}
|
|
141
|
+
`;
|
|
142
|
+
};
|
|
143
|
+
|
|
89
144
|
const withLocalOpenIAP: ConfigPlugin<
|
|
90
145
|
{
|
|
91
146
|
localPath?: LocalPathOption;
|
|
@@ -203,10 +258,18 @@ const withLocalOpenIAP: ConfigPlugin<
|
|
|
203
258
|
|
|
204
259
|
// 1) settings.gradle: include and map projectDir
|
|
205
260
|
const settings = config.modResults;
|
|
206
|
-
const
|
|
207
|
-
const
|
|
261
|
+
const settingsLanguage = normalizeGradleLanguage(settings.language);
|
|
262
|
+
const includeLine =
|
|
263
|
+
settingsLanguage === 'kotlin'
|
|
264
|
+
? 'include(":openiap-google")'
|
|
265
|
+
: "include ':openiap-google'";
|
|
266
|
+
const projectDirLine =
|
|
267
|
+
settingsLanguage === 'kotlin'
|
|
268
|
+
? `project(":openiap-google").projectDir = File(settingsDir, "${relativeAndroidModulePath}")`
|
|
269
|
+
: `project(':openiap-google').projectDir = new File(settingsDir, '${relativeAndroidModulePath}')`;
|
|
270
|
+
const includePattern = /include\s*(?:\(\s*)?["']:openiap-google["']\s*\)?/;
|
|
208
271
|
const projectDirPattern =
|
|
209
|
-
|
|
272
|
+
/^\s*project\(["']:openiap-google["']\)\.projectDir\s*=.*$/gm;
|
|
210
273
|
let contents = settings.contents ?? '';
|
|
211
274
|
|
|
212
275
|
// Ensure pluginManagement has plugin mappings required by the included module
|
|
@@ -279,7 +342,7 @@ const withLocalOpenIAP: ConfigPlugin<
|
|
|
279
342
|
};
|
|
280
343
|
|
|
281
344
|
injectPluginManagement();
|
|
282
|
-
if (!
|
|
345
|
+
if (!includePattern.test(contents)) contents += `\n${includeLine}\n`;
|
|
283
346
|
if (projectDirPattern.test(contents)) {
|
|
284
347
|
contents = contents.replace(projectDirPattern, projectDirLine);
|
|
285
348
|
} else if (!contents.includes(projectDirLine)) {
|
|
@@ -305,9 +368,16 @@ const withLocalOpenIAP: ConfigPlugin<
|
|
|
305
368
|
}
|
|
306
369
|
|
|
307
370
|
const gradle = config.modResults;
|
|
308
|
-
const
|
|
371
|
+
const appLanguage = normalizeGradleLanguage(gradle.language);
|
|
372
|
+
const dependencyLine =
|
|
373
|
+
appLanguage === 'kotlin'
|
|
374
|
+
? ` implementation(project(":openiap-google"))`
|
|
375
|
+
: ` implementation project(':openiap-google')`;
|
|
309
376
|
const flavor = props?.isHorizonEnabled ? 'horizon' : 'play';
|
|
310
|
-
const strategyLine =
|
|
377
|
+
const strategyLine =
|
|
378
|
+
appLanguage === 'kotlin'
|
|
379
|
+
? ` missingDimensionStrategy("platform", "${flavor}")`
|
|
380
|
+
: ` missingDimensionStrategy "platform", "${flavor}"`;
|
|
311
381
|
|
|
312
382
|
let contents = gradle.contents;
|
|
313
383
|
|
|
@@ -358,6 +428,31 @@ const withLocalOpenIAP: ConfigPlugin<
|
|
|
358
428
|
return config;
|
|
359
429
|
});
|
|
360
430
|
|
|
431
|
+
// 2b) project build.gradle: Expo autolinked library modules can consume the
|
|
432
|
+
// local flavored OpenIAP module transitively, so give them the same default.
|
|
433
|
+
config = withProjectBuildGradle(config, (config) => {
|
|
434
|
+
const projectRoot = (config.modRequest as any).projectRoot as string;
|
|
435
|
+
const raw = props?.localPath;
|
|
436
|
+
const androidInput = typeof raw === 'string' ? undefined : raw?.android;
|
|
437
|
+
const androidModulePath =
|
|
438
|
+
resolveAndroidModulePath(androidInput) ||
|
|
439
|
+
resolveAndroidModulePath(path.resolve(projectRoot, 'openiap-google')) ||
|
|
440
|
+
null;
|
|
441
|
+
|
|
442
|
+
if (!androidModulePath || !fs.existsSync(androidModulePath)) {
|
|
443
|
+
return config;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const flavor = props?.isHorizonEnabled ? 'horizon' : 'play';
|
|
447
|
+
config.modResults.contents = ensureLocalOpenIapFlavorStrategy(
|
|
448
|
+
config.modResults.contents,
|
|
449
|
+
flavor,
|
|
450
|
+
normalizeGradleLanguage(config.modResults.language),
|
|
451
|
+
);
|
|
452
|
+
logOnce(`🛠️ expo-iap: Added local OpenIAP flavor strategy for ${flavor}`);
|
|
453
|
+
return config;
|
|
454
|
+
});
|
|
455
|
+
|
|
361
456
|
// 3) Set horizonEnabled in gradle.properties
|
|
362
457
|
config = withDangerousMod(config, [
|
|
363
458
|
'android',
|