react-native-iap 15.4.0-rc.1 → 15.4.0-rc.3

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.
@@ -1477,7 +1477,7 @@ class HybridRnIap : HybridRnIapSpec() {
1477
1477
  val propsMap = mutableMapOf<String, Any?>("provider" to providerString)
1478
1478
  (params.iapkit as? Variant_NullType_NitroVerifyPurchaseWithIapkitProps.Second)?.value?.let { iapkit ->
1479
1479
  val iapkitMap = mutableMapOf<String, Any?>()
1480
- // Use provided apiKey, or fallback to AndroidManifest meta-data (set by config plugin)
1480
+ // Use provided apiKey, or fallback to host app AndroidManifest meta-data.
1481
1481
  val apiKey = iapkit.apiKey.unwrapString() ?: getIapkitApiKeyFromManifest()
1482
1482
  apiKey?.let { iapkitMap["apiKey"] = it }
1483
1483
  (iapkit.google as? Variant_NullType_NitroVerifyPurchaseWithIapkitGoogleProps.Second)?.value?.let { google ->
@@ -2025,8 +2025,8 @@ class HybridRnIap : HybridRnIapSpec() {
2025
2025
  }
2026
2026
 
2027
2027
  /**
2028
- * Read IAPKit API key from AndroidManifest.xml meta-data (set by config plugin).
2029
- * Config plugin sets: <meta-data android:name="dev.iapkit.API_KEY" android:value="..." />
2028
+ * Read IAPKit API key from AndroidManifest.xml meta-data.
2029
+ * Host app sets: <meta-data android:name="dev.iapkit.API_KEY" android:value="..." />
2030
2030
  */
2031
2031
  private fun getIapkitApiKeyFromManifest(): String? {
2032
2032
  return try {
@@ -434,7 +434,7 @@ class HybridRnIap: HybridRnIapSpec {
434
434
  var propsDict: [String: Any] = ["provider": params.provider.stringValue]
435
435
  if case .second(let iapkit) = params.iapkit {
436
436
  var iapkitDict: [String: Any] = [:]
437
- // Use provided apiKey, or fallback to Info.plist IAPKitAPIKey (set by config plugin)
437
+ // Use provided apiKey, or fallback to the host app's Info.plist IAPKitAPIKey.
438
438
  if case .second(let apiKey) = iapkit.apiKey {
439
439
  iapkitDict["apiKey"] = apiKey
440
440
  } else if let plistApiKey = Bundle.main.object(forInfoDictionaryKey: "IAPKitAPIKey") as? String {
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec": "2.0.3",
2
+ "spec": "2.0.4-rc.1",
3
3
  "google": "2.3.0-rc.1",
4
- "apple": "2.2.4"
4
+ "apple": "2.2.5-rc.1"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-iap",
3
- "version": "15.4.0-rc.1",
3
+ "version": "15.4.0-rc.3",
4
4
  "description": "React Native In-App Purchases module for iOS and Android using Nitro",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -11,14 +11,11 @@
11
11
  "types": "./lib/typescript/src/index.d.ts",
12
12
  "default": "./lib/module/index.js"
13
13
  },
14
- "./plugin": "./app.plugin.js",
15
- "./app.plugin.js": "./app.plugin.js",
16
14
  "./package.json": "./package.json"
17
15
  },
18
16
  "files": [
19
17
  "src",
20
18
  "lib",
21
- "plugin",
22
19
  "android",
23
20
  "ios",
24
21
  "cpp",
@@ -26,7 +23,6 @@
26
23
  "nitro.json",
27
24
  "openiap-versions.json",
28
25
  "*.podspec",
29
- "app.plugin.js",
30
26
  "react-native.config.js",
31
27
  "!ios/build",
32
28
  "!android/build",
@@ -43,7 +39,6 @@
43
39
  "!**/.*"
44
40
  ],
45
41
  "scripts": {
46
- "build:plugin": "tsc --build plugin",
47
42
  "example": "yarn workspace rn-iap-example",
48
43
  "example:ios": "yarn workspace rn-iap-example ios",
49
44
  "example:android": "yarn workspace rn-iap-example android",
@@ -52,21 +47,20 @@
52
47
  "typecheck:lib": "yarn tsc --noEmit",
53
48
  "clean": "rm -rf android/build node_modules/**/android/build lib nitrogen/generated",
54
49
  "lint": "eslint --ext .ts,.tsx,.js,.jsx src",
55
- "lint:eslint": "eslint --fix 'src/**/*.{ts,tsx}' 'plugin/src/**/*.{ts,tsx}'",
50
+ "lint:eslint": "eslint --fix 'src/**/*.{ts,tsx}'",
56
51
  "lint:prettier": "prettier --write \"**/*.{md,js,jsx,ts,tsx}\"",
57
52
  "lint:tsc": "tsc -p tsconfig.json --noEmit --skipLibCheck",
58
53
  "lint:ci": "yarn lint:tsc && yarn lint:eslint && yarn lint:prettier",
59
- "prepare": "npx tsx scripts/check-nitro-versions.ts && bob build && yarn nitrogen && yarn build:plugin",
54
+ "prepare": "npx tsx scripts/check-nitro-versions.ts && bob build && yarn nitrogen",
60
55
  "nitrogen": "node --stack-size=65536 node_modules/nitrogen/lib/index.js",
61
56
  "specs": "yarn nitrogen --logLevel=\"debug\"",
62
57
  "test": "jest --coverage",
63
58
  "test:library": "jest --coverage",
64
59
  "test:example": "yarn workspace rn-iap-example test --coverage",
65
- "test:plugin": "jest plugin/__tests__ --runInBand --coverage",
66
60
  "test:all": "yarn test:library && yarn test:example",
67
61
  "test:ci": "jest --maxWorkers=2 --coverage",
68
62
  "test:ci:example": "yarn workspace rn-iap-example test --coverage",
69
- "verify:consumer-install": "node ../../scripts/verify-npm-consumer-install.mjs --package . --package-name react-native-iap --required openiap-versions.json --required lib/module/index.js --required lib/typescript/src/index.d.ts --required android/build.gradle --required NitroIap.podspec --required app.plugin.js --required nitro.json",
63
+ "verify:consumer-install": "node ../../scripts/verify-npm-consumer-install.mjs --package . --package-name react-native-iap --required openiap-versions.json --required lib/module/index.js --required lib/typescript/src/index.d.ts --required android/build.gradle --required NitroIap.podspec --required nitro.json",
70
64
  "prepare:husky": "husky install",
71
65
  "precommit": "lint-staged",
72
66
  "ci:check": "./scripts/ci-check.sh",
package/app.plugin.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./plugin/build/withIAP.js');
@@ -1,81 +0,0 @@
1
- import type { ConfigPlugin } from 'expo/config-plugins';
2
- import type { ExpoConfig } from '@expo/config-types';
3
- export declare const modifyProjectBuildGradle: (gradle: string) => string;
4
- export interface IosAlternativeBillingConfig {
5
- /** Country codes where external purchases are supported (ISO 3166-1 alpha-2) */
6
- countries?: string[];
7
- /** External purchase URLs per country (iOS 15.4+) */
8
- links?: Record<string, string>;
9
- /** Multiple external purchase URLs per country (iOS 17.5+, up to 5 per country) */
10
- multiLinks?: Record<string, string[]>;
11
- /** Custom link regions (iOS 18.1+) */
12
- customLinkRegions?: string[];
13
- /** Streaming link regions for music apps (iOS 18.2+) */
14
- streamingLinkRegions?: string[];
15
- /** Enable external purchase link entitlement */
16
- enableExternalPurchaseLink?: boolean;
17
- /** Enable external purchase link streaming entitlement (music apps only) */
18
- enableExternalPurchaseLinkStreaming?: boolean;
19
- }
20
- /** Add external purchase entitlements and Info.plist configuration */
21
- declare const withIosAlternativeBilling: ConfigPlugin<IosAlternativeBillingConfig | undefined>;
22
- type IapPluginProps = {
23
- ios?: {
24
- 'with-folly-no-coroutines'?: boolean;
25
- 'with-folly-no-couroutines'?: boolean;
26
- };
27
- /**
28
- * iOS Alternative Billing configuration.
29
- * Configure external purchase countries, links, and entitlements.
30
- * Requires approval from Apple.
31
- * @platform ios
32
- */
33
- iosAlternativeBilling?: IosAlternativeBillingConfig;
34
- /**
35
- * IAPKit project key for managed purchase verification.
36
- * This key will be added to AndroidManifest.xml (as meta-data) and Info.plist.
37
- * Get your project key from https://kit.openiap.dev
38
- */
39
- iapkitApiKey?: string;
40
- /**
41
- * Amazon platform targets.
42
- *
43
- * Fire OS selects the Android Amazon Appstore flavor. Vega OS is selected by
44
- * the Kepler runtime and does not change the Android Gradle flavor.
45
- */
46
- amazon?: {
47
- /**
48
- * Fire OS module for Amazon-distributed Android builds.
49
- * @platform android
50
- */
51
- fireOS?: boolean;
52
- /**
53
- * Vega OS runtime target. This is not an Android flavor.
54
- */
55
- vegaOS?: boolean;
56
- };
57
- /**
58
- * Optional non-Amazon Android store modules.
59
- * Fire OS takes precedence when both Fire OS and Horizon are enabled.
60
- */
61
- modules?: {
62
- /**
63
- * Horizon module for Meta Quest/VR devices.
64
- * @platform android
65
- */
66
- horizon?: boolean;
67
- };
68
- };
69
- export declare function resolveAmazonPlatformFlags(props?: IapPluginProps): {
70
- isFireOsEnabled: boolean;
71
- isVegaEnabled: boolean;
72
- isHorizonEnabled: boolean;
73
- };
74
- type IapPluginCallable = {
75
- (config: ExpoConfig): ExpoConfig;
76
- (config: ExpoConfig, props?: IapPluginProps): ExpoConfig;
77
- };
78
- declare const pluginExport: IapPluginCallable;
79
- export { withIosAlternativeBilling };
80
- export { pluginExport as default };
81
- //# sourceMappingURL=withIAP.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"withIAP.d.ts","sourceRoot":"","sources":["../../../../plugin/src/withIAP.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,oBAAoB,CAAC;AA0BnD,eAAO,MAAM,wBAAwB,GAAI,QAAQ,MAAM,KAAG,MAWzD,CAAC;AAqOF,MAAM,WAAW,2BAA2B;IAC1C,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,sCAAsC;IACtC,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,wDAAwD;IACxD,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,gDAAgD;IAChD,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,4EAA4E;IAC5E,mCAAmC,CAAC,EAAE,OAAO,CAAC;CAC/C;AAED,sEAAsE;AACtE,QAAA,MAAM,yBAAyB,EAAE,YAAY,CAC3C,2BAA2B,GAAG,SAAS,CA+HxC,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,GAAG,CAAC,EAAE;QAEJ,0BAA0B,CAAC,EAAE,OAAO,CAAC;QAErC,2BAA2B,CAAC,EAAE,OAAO,CAAC;KACvC,CAAC;IACF;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,2BAA2B,CAAC;IACpD;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QACP;;;WAGG;QACH,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB;;WAEG;QACH,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;IACF;;;OAGG;IACH,OAAO,CAAC,EAAE;QACR;;;WAGG;QACH,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;CACH,CAAC;AAEF,wBAAgB,0BAA0B,CAAC,KAAK,CAAC,EAAE,cAAc,GAAG;IAClE,eAAe,EAAE,OAAO,CAAC;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;CAC3B,CAYA;AAsHD,KAAK,iBAAiB,GAAG;IACvB,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAAC;IACjC,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,cAAc,GAAG,UAAU,CAAC;CAC1D,CAAC;AAWF,QAAA,MAAM,YAAY,EAAE,iBAGyC,CAAC;AAE9D,OAAO,EAAC,yBAAyB,EAAC,CAAC;AACnC,OAAO,EAAC,YAAY,IAAI,OAAO,EAAC,CAAC"}
@@ -1,80 +0,0 @@
1
- import type { ConfigPlugin } from 'expo/config-plugins';
2
- import type { ExpoConfig } from '@expo/config-types';
3
- export declare const modifyProjectBuildGradle: (gradle: string) => string;
4
- export interface IosAlternativeBillingConfig {
5
- /** Country codes where external purchases are supported (ISO 3166-1 alpha-2) */
6
- countries?: string[];
7
- /** External purchase URLs per country (iOS 15.4+) */
8
- links?: Record<string, string>;
9
- /** Multiple external purchase URLs per country (iOS 17.5+, up to 5 per country) */
10
- multiLinks?: Record<string, string[]>;
11
- /** Custom link regions (iOS 18.1+) */
12
- customLinkRegions?: string[];
13
- /** Streaming link regions for music apps (iOS 18.2+) */
14
- streamingLinkRegions?: string[];
15
- /** Enable external purchase link entitlement */
16
- enableExternalPurchaseLink?: boolean;
17
- /** Enable external purchase link streaming entitlement (music apps only) */
18
- enableExternalPurchaseLinkStreaming?: boolean;
19
- }
20
- /** Add external purchase entitlements and Info.plist configuration */
21
- declare const withIosAlternativeBilling: ConfigPlugin<IosAlternativeBillingConfig | undefined>;
22
- type IapPluginProps = {
23
- ios?: {
24
- 'with-folly-no-coroutines'?: boolean;
25
- 'with-folly-no-couroutines'?: boolean;
26
- };
27
- /**
28
- * iOS Alternative Billing configuration.
29
- * Configure external purchase countries, links, and entitlements.
30
- * Requires approval from Apple.
31
- * @platform ios
32
- */
33
- iosAlternativeBilling?: IosAlternativeBillingConfig;
34
- /**
35
- * IAPKit project key for managed purchase verification.
36
- * This key will be added to AndroidManifest.xml (as meta-data) and Info.plist.
37
- * Get your project key from https://kit.openiap.dev
38
- */
39
- iapkitApiKey?: string;
40
- /**
41
- * Amazon platform targets.
42
- *
43
- * Fire OS selects the Android Amazon Appstore flavor. Vega OS is selected by
44
- * the Kepler runtime and does not change the Android Gradle flavor.
45
- */
46
- amazon?: {
47
- /**
48
- * Fire OS module for Amazon-distributed Android builds.
49
- * @platform android
50
- */
51
- fireOS?: boolean;
52
- /**
53
- * Vega OS runtime target. This is not an Android flavor.
54
- */
55
- vegaOS?: boolean;
56
- };
57
- /**
58
- * Optional non-Amazon Android store modules.
59
- * Fire OS takes precedence when both Fire OS and Horizon are enabled.
60
- */
61
- modules?: {
62
- /**
63
- * Horizon module for Meta Quest/VR devices.
64
- * @platform android
65
- */
66
- horizon?: boolean;
67
- };
68
- };
69
- export declare function resolveAmazonPlatformFlags(props?: IapPluginProps): {
70
- isFireOsEnabled: boolean;
71
- isVegaEnabled: boolean;
72
- isHorizonEnabled: boolean;
73
- };
74
- type IapPluginCallable = {
75
- (config: ExpoConfig): ExpoConfig;
76
- (config: ExpoConfig, props?: IapPluginProps): ExpoConfig;
77
- };
78
- declare const pluginExport: IapPluginCallable;
79
- export { withIosAlternativeBilling };
80
- export { pluginExport as default };
@@ -1,381 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = exports.withIosAlternativeBilling = exports.modifyProjectBuildGradle = void 0;
4
- exports.resolveAmazonPlatformFlags = resolveAmazonPlatformFlags;
5
- const config_plugins_1 = require("expo/config-plugins");
6
- const pkg = require('../../package.json');
7
- // Global flag to prevent duplicate logs
8
- let hasLoggedPluginExecution = false;
9
- const addLineToGradle = (content, anchor, lineToAdd, offset = 1) => {
10
- const lines = content.split('\n');
11
- const index = lines.findIndex((line) => line.match(anchor));
12
- if (index === -1) {
13
- console.warn(`Anchor "${anchor}" not found in build.gradle. Appending to end.`);
14
- lines.push(lineToAdd);
15
- }
16
- else {
17
- lines.splice(index + offset, 0, lineToAdd);
18
- }
19
- return lines.join('\n');
20
- };
21
- const modifyProjectBuildGradle = (gradle) => {
22
- // Keep backward-compatible behavior: add supportLibVersion inside ext { } if missing
23
- if (!gradle.includes('supportLibVersion')) {
24
- const lines = gradle.split('\n');
25
- const extIndex = lines.findIndex((line) => line.trim() === 'ext {');
26
- if (extIndex !== -1) {
27
- lines.splice(extIndex + 1, 0, 'supportLibVersion = "28.0.0"');
28
- return lines.join('\n');
29
- }
30
- }
31
- return gradle;
32
- };
33
- exports.modifyProjectBuildGradle = modifyProjectBuildGradle;
34
- const OPENIAP_GROUP = 'io.github.hyochan.openiap';
35
- const modifyAppBuildGradle = (gradle, isHorizonEnabled, isFireOsEnabled) => {
36
- const escapeRegExp = (value) => {
37
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
38
- };
39
- function loadOpenIapVersion() {
40
- try {
41
- const parsed = require('../../openiap-versions.json');
42
- const googleVersion = typeof parsed?.google === 'string' ? parsed.google.trim() : '';
43
- if (!googleVersion) {
44
- throw new Error('react-native-iap: "google" version missing or invalid in openiap-versions.json');
45
- }
46
- return googleVersion;
47
- }
48
- catch (error) {
49
- throw new Error(`react-native-iap: Unable to load openiap-versions.json (${error instanceof Error ? error.message : error})`);
50
- }
51
- }
52
- let modified = gradle;
53
- let openiapVersion;
54
- try {
55
- openiapVersion = loadOpenIapVersion();
56
- }
57
- catch (error) {
58
- config_plugins_1.WarningAggregator.addWarningAndroid('react-native-iap', `react-native-iap: Failed to resolve OpenIAP version (${error instanceof Error ? error.message : error})`);
59
- return gradle;
60
- }
61
- // Replace legacy Billing/GMS instructions with OpenIAP Google library
62
- // Remove any old billingclient or play-services-base lines we may have added previously
63
- modified = modified
64
- .replace(/^[ \t]*(implementation|api)[ \t]+["']com\.android\.billingclient:billing-ktx:[^"']+["'][ \t]*$/gim, '')
65
- .replace(/^[ \t]*(implementation|api)[ \t]+["']com\.google\.android\.gms:play-services-base:[^"']+["'][ \t]*$/gim, '')
66
- .replace(/\n{3,}/g, '\n\n');
67
- const flavor = isFireOsEnabled
68
- ? 'amazon'
69
- : isHorizonEnabled
70
- ? 'horizon'
71
- : 'play';
72
- const artifactId = isFireOsEnabled
73
- ? 'openiap-google-amazon'
74
- : isHorizonEnabled
75
- ? 'openiap-google-horizon'
76
- : 'openiap-google';
77
- const openiapCoord = `${OPENIAP_GROUP}:${artifactId}`;
78
- const openiapDep = ` implementation "${openiapCoord}:${openiapVersion}"`;
79
- const desiredOpeniapLine = new RegExp(`^[ \\t]*(?:implementation|api)[ \\t]+\\(?["']${escapeRegExp(`${openiapCoord}:${openiapVersion}`)}["']\\)?[ \\t]*$`, 'm');
80
- const openiapAnyLine = /^[ \t]*(implementation|api)[ \t]+\(?["']io\.github\.hyochan\.openiap:openiap-google(?:-(?:horizon|amazon))?:[^"']+["']\)?[ \t]*$/gim;
81
- const withoutExistingOpeniap = modified.replace(openiapAnyLine, '');
82
- const hadExistingOpeniap = withoutExistingOpeniap !== modified;
83
- if (hadExistingOpeniap) {
84
- modified = withoutExistingOpeniap.replace(/\n{3,}/g, '\n\n');
85
- }
86
- if (!desiredOpeniapLine.test(modified)) {
87
- if (!/dependencies\s*{/.test(modified)) {
88
- modified += `\n\ndependencies {\n${openiapDep}\n}\n`;
89
- }
90
- else {
91
- modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep);
92
- }
93
- if (!hasLoggedPluginExecution) {
94
- console.log(`🛠️ react-native-iap: Added OpenIAP (${openiapVersion}) to build.gradle`);
95
- }
96
- }
97
- // Remove stale OpenIAP platform strategies even when returning to the default
98
- // Play artifact. Otherwise a previous Fire OS/Horizon prebuild can keep
99
- // selecting the wrong local flavor.
100
- const strategyPattern = /^[ \t]*missingDimensionStrategy[ \t]*\(?[ \t]*["']platform["'][ \t]*,[ \t]*["'](play|horizon|amazon)["'][ \t]*\)?[ \t]*$/gm;
101
- const withoutExistingStrategy = modified.replace(strategyPattern, '');
102
- if (withoutExistingStrategy !== modified) {
103
- modified = withoutExistingStrategy;
104
- }
105
- const defaultConfigRegex = /defaultConfig\s*{/;
106
- if (defaultConfigRegex.test(modified)) {
107
- const strategyLine = ` missingDimensionStrategy "platform", "${flavor}"`;
108
- if (!/missingDimensionStrategy.*platform/.test(modified)) {
109
- modified = addLineToGradle(modified, defaultConfigRegex, strategyLine);
110
- }
111
- }
112
- return modified;
113
- };
114
- const withIapAndroid = (config, props) => {
115
- // Add OpenIAP dependency to app build.gradle
116
- config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
117
- config.modResults.contents = modifyAppBuildGradle(config.modResults.contents, props?.isHorizonEnabled, props?.isFireOsEnabled);
118
- return config;
119
- });
120
- config = (0, config_plugins_1.withGradleProperties)(config, (config) => {
121
- const horizonValue = props?.isHorizonEnabled ?? false;
122
- const fireOsValue = props?.isFireOsEnabled ?? false;
123
- config.modResults = config.modResults.filter((item) => item.type !== 'property' ||
124
- !['horizonEnabled', 'fireOsEnabled'].includes(item.key));
125
- config.modResults.push({
126
- type: 'property',
127
- key: 'horizonEnabled',
128
- value: String(horizonValue),
129
- });
130
- config.modResults.push({
131
- type: 'property',
132
- key: 'fireOsEnabled',
133
- value: String(fireOsValue),
134
- });
135
- return config;
136
- });
137
- config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
138
- const manifest = config.modResults;
139
- const existingPermissions = manifest.manifest['uses-permission'];
140
- const permissions = Array.isArray(existingPermissions)
141
- ? existingPermissions
142
- : existingPermissions
143
- ? [existingPermissions]
144
- : [];
145
- manifest.manifest['uses-permission'] = permissions;
146
- const billingPerm = { $: { 'android:name': 'com.android.vending.BILLING' } };
147
- if (props?.isFireOsEnabled) {
148
- manifest.manifest['uses-permission'] = permissions.filter((p) => p.$['android:name'] !== 'com.android.vending.BILLING');
149
- }
150
- else {
151
- const alreadyExists = permissions.some((p) => p.$['android:name'] === 'com.android.vending.BILLING');
152
- if (!alreadyExists) {
153
- permissions.push(billingPerm);
154
- if (!hasLoggedPluginExecution) {
155
- console.log('✅ Added com.android.vending.BILLING to AndroidManifest.xml');
156
- }
157
- }
158
- else {
159
- if (!hasLoggedPluginExecution) {
160
- console.log('ℹ️ com.android.vending.BILLING already exists in AndroidManifest.xml');
161
- }
162
- }
163
- }
164
- // Add IAPKit API key as meta-data if provided
165
- if (props?.iapkitApiKey) {
166
- const application = manifest.manifest.application;
167
- const app = application?.[0];
168
- if (app) {
169
- if (!app['meta-data']) {
170
- app['meta-data'] = [];
171
- }
172
- const metaDataKey = 'dev.iapkit.API_KEY';
173
- const existingMetaData = app['meta-data'].find((m) => m.$?.['android:name'] === metaDataKey);
174
- if (!existingMetaData) {
175
- app['meta-data'].push({
176
- $: {
177
- 'android:name': metaDataKey,
178
- 'android:value': props.iapkitApiKey,
179
- },
180
- });
181
- if (!hasLoggedPluginExecution) {
182
- console.log('✅ Added IAPKit API key to AndroidManifest.xml');
183
- }
184
- }
185
- }
186
- }
187
- return config;
188
- });
189
- return config;
190
- };
191
- /** Add external purchase entitlements and Info.plist configuration */
192
- const withIosAlternativeBilling = (config, options) => {
193
- if (!options || !options.countries || options.countries.length === 0) {
194
- return config;
195
- }
196
- // Add entitlements
197
- config = (0, config_plugins_1.withEntitlementsPlist)(config, (config) => {
198
- // Always add basic external purchase entitlement when countries are specified
199
- config.modResults['com.apple.developer.storekit.external-purchase'] = true;
200
- if (!hasLoggedPluginExecution) {
201
- console.log('✅ Added com.apple.developer.storekit.external-purchase to entitlements');
202
- }
203
- // Add external purchase link entitlement if enabled
204
- if (options.enableExternalPurchaseLink) {
205
- config.modResults['com.apple.developer.storekit.external-purchase-link'] =
206
- true;
207
- if (!hasLoggedPluginExecution) {
208
- console.log('✅ Added com.apple.developer.storekit.external-purchase-link to entitlements');
209
- }
210
- }
211
- // Add streaming entitlement if enabled
212
- if (options.enableExternalPurchaseLinkStreaming) {
213
- config.modResults['com.apple.developer.storekit.external-purchase-link-streaming'] = true;
214
- if (!hasLoggedPluginExecution) {
215
- console.log('✅ Added com.apple.developer.storekit.external-purchase-link-streaming to entitlements');
216
- }
217
- }
218
- return config;
219
- });
220
- // Add Info.plist configuration
221
- config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
222
- const plist = config.modResults;
223
- // Helper function to normalize country codes to uppercase
224
- const normalize = (code) => code.trim().toUpperCase();
225
- // 1. SKExternalPurchase (Required)
226
- const normalizedCountries = options.countries?.map(normalize);
227
- plist.SKExternalPurchase = normalizedCountries;
228
- if (!hasLoggedPluginExecution) {
229
- console.log(`✅ Added SKExternalPurchase with countries: ${normalizedCountries?.join(', ')}`);
230
- }
231
- // 2. SKExternalPurchaseLink (Optional - iOS 15.4+)
232
- if (options.links && Object.keys(options.links).length > 0) {
233
- plist.SKExternalPurchaseLink = Object.fromEntries(Object.entries(options.links).map(([code, url]) => [
234
- normalize(code),
235
- url,
236
- ]));
237
- if (!hasLoggedPluginExecution) {
238
- console.log(`✅ Added SKExternalPurchaseLink for ${Object.keys(options.links).length} countries`);
239
- }
240
- }
241
- // 3. SKExternalPurchaseMultiLink (iOS 17.5+)
242
- if (options.multiLinks && Object.keys(options.multiLinks).length > 0) {
243
- plist.SKExternalPurchaseMultiLink = Object.fromEntries(Object.entries(options.multiLinks).map(([code, urls]) => [
244
- normalize(code),
245
- urls,
246
- ]));
247
- if (!hasLoggedPluginExecution) {
248
- console.log(`✅ Added SKExternalPurchaseMultiLink for ${Object.keys(options.multiLinks).length} countries`);
249
- }
250
- }
251
- // 4. SKExternalPurchaseCustomLinkRegions (iOS 18.1+)
252
- if (options.customLinkRegions && options.customLinkRegions.length > 0) {
253
- plist.SKExternalPurchaseCustomLinkRegions =
254
- options.customLinkRegions.map(normalize);
255
- if (!hasLoggedPluginExecution) {
256
- console.log(`✅ Added SKExternalPurchaseCustomLinkRegions: ${options.customLinkRegions
257
- .map(normalize)
258
- .join(', ')}`);
259
- }
260
- }
261
- // 5. SKExternalPurchaseLinkStreamingRegions (iOS 18.2+)
262
- if (options.streamingLinkRegions &&
263
- options.streamingLinkRegions.length > 0) {
264
- plist.SKExternalPurchaseLinkStreamingRegions =
265
- options.streamingLinkRegions.map(normalize);
266
- if (!hasLoggedPluginExecution) {
267
- console.log(`✅ Added SKExternalPurchaseLinkStreamingRegions: ${options.streamingLinkRegions
268
- .map(normalize)
269
- .join(', ')}`);
270
- }
271
- }
272
- return config;
273
- });
274
- return config;
275
- };
276
- exports.withIosAlternativeBilling = withIosAlternativeBilling;
277
- function resolveAmazonPlatformFlags(props) {
278
- const isFireOsEnabled = props?.amazon?.fireOS ?? false;
279
- const isVegaEnabled = props?.amazon?.vegaOS ?? false;
280
- const isHorizonEnabled = isFireOsEnabled
281
- ? false
282
- : (props?.modules?.horizon ?? false);
283
- return {
284
- isFireOsEnabled,
285
- isVegaEnabled,
286
- isHorizonEnabled,
287
- };
288
- }
289
- const withIapIosFollyWorkaround = (config, props) => {
290
- const newKey = props?.ios?.['with-folly-no-coroutines'];
291
- const oldKey = props?.ios?.['with-folly-no-couroutines'];
292
- if (oldKey && !hasLoggedPluginExecution) {
293
- // Temporary deprecation notice; remove when old key is dropped
294
- config_plugins_1.WarningAggregator.addWarningIOS('react-native-iap', "react-native-iap: 'ios.with-folly-no-couroutines' is deprecated; use 'ios.with-folly-no-coroutines'.");
295
- }
296
- const enabled = !!(newKey ?? oldKey);
297
- if (!enabled)
298
- return config;
299
- return (0, config_plugins_1.withPodfile)(config, (config) => {
300
- let contents = config.modResults.contents;
301
- // Idempotency: if any of the defines already exists, assume it's applied
302
- if (contents.includes('FOLLY_CFG_NO_COROUTINES') ||
303
- contents.includes('FOLLY_HAS_COROUTINES=0')) {
304
- return config;
305
- }
306
- const anchor = 'post_install do |installer|';
307
- const snippet = `
308
- # react-native-iap (expo): Disable Folly coroutines to avoid including non-vendored <folly/coro/*> headers
309
- installer.pods_project.targets.each do |target|
310
- target.build_configurations.each do |config|
311
- defs = (config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] || ['$(inherited)'])
312
- defs << 'FOLLY_NO_CONFIG=1' unless defs.any? { |d| d.to_s.include?('FOLLY_NO_CONFIG') }
313
- # Portability.h respects FOLLY_CFG_NO_COROUTINES to fully disable coroutine support
314
- defs << 'FOLLY_CFG_NO_COROUTINES=1' unless defs.any? { |d| d.to_s.include?('FOLLY_CFG_NO_COROUTINES') }
315
- defs << 'FOLLY_HAS_COROUTINES=0' unless defs.any? { |d| d.to_s.include?('FOLLY_HAS_COROUTINES') }
316
- config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = defs
317
- end
318
- end`;
319
- if (contents.includes(anchor)) {
320
- contents = contents.replace(anchor, `${anchor}\n${snippet}`);
321
- }
322
- else {
323
- // As a fallback, append a new post_install block
324
- contents += `
325
-
326
- ${anchor}
327
- ${snippet}
328
- end
329
- `;
330
- }
331
- config.modResults.contents = contents;
332
- return config;
333
- });
334
- };
335
- /** Add IAPKit API key to iOS Info.plist */
336
- const withIapkitApiKeyIOS = (config, apiKey) => {
337
- if (!apiKey) {
338
- return config;
339
- }
340
- return (0, config_plugins_1.withInfoPlist)(config, (config) => {
341
- const plist = config.modResults;
342
- const plistKey = 'IAPKitAPIKey';
343
- if (!plist[plistKey]) {
344
- plist[plistKey] = apiKey;
345
- if (!hasLoggedPluginExecution) {
346
- console.log('✅ Added IAPKit API key to Info.plist');
347
- }
348
- }
349
- return config;
350
- });
351
- };
352
- const withIAP = (config, props) => {
353
- try {
354
- const { isFireOsEnabled, isHorizonEnabled } = resolveAmazonPlatformFlags(props);
355
- let result = withIapAndroid(config, {
356
- iapkitApiKey: props?.iapkitApiKey,
357
- isHorizonEnabled,
358
- isFireOsEnabled,
359
- });
360
- result = withIapIosFollyWorkaround(result, props);
361
- // Add iOS alternative billing configuration if provided
362
- if (props?.iosAlternativeBilling) {
363
- result = withIosAlternativeBilling(result, props.iosAlternativeBilling);
364
- }
365
- // Add IAPKit API key to iOS Info.plist if provided
366
- if (props?.iapkitApiKey) {
367
- result = withIapkitApiKeyIOS(result, props.iapkitApiKey);
368
- }
369
- // Set flag after first execution to prevent duplicate logs
370
- hasLoggedPluginExecution = true;
371
- return result;
372
- }
373
- catch (error) {
374
- config_plugins_1.WarningAggregator.addWarningAndroid('react-native-iap', `react-native-iap plugin encountered an error: ${error}`);
375
- console.error('react-native-iap plugin error:', error);
376
- return config;
377
- }
378
- };
379
- const _wrapped = (0, config_plugins_1.createRunOncePlugin)(withIAP, pkg.name, pkg.version);
380
- const pluginExport = ((config, props) => _wrapped(config, props));
381
- exports.default = pluginExport;
@@ -1,630 +0,0 @@
1
- import {
2
- createRunOncePlugin,
3
- WarningAggregator,
4
- withAndroidManifest,
5
- withAppBuildGradle,
6
- withGradleProperties,
7
- withPodfile,
8
- withEntitlementsPlist,
9
- withInfoPlist,
10
- } from 'expo/config-plugins';
11
- import type {ConfigPlugin} from 'expo/config-plugins';
12
- import type {ExpoConfig} from '@expo/config-types';
13
-
14
- const pkg = require('../../package.json');
15
-
16
- // Global flag to prevent duplicate logs
17
- let hasLoggedPluginExecution = false;
18
-
19
- const addLineToGradle = (
20
- content: string,
21
- anchor: RegExp | string,
22
- lineToAdd: string,
23
- offset: number = 1,
24
- ): string => {
25
- const lines = content.split('\n');
26
- const index = lines.findIndex((line) => line.match(anchor));
27
- if (index === -1) {
28
- console.warn(
29
- `Anchor "${anchor}" not found in build.gradle. Appending to end.`,
30
- );
31
- lines.push(lineToAdd);
32
- } else {
33
- lines.splice(index + offset, 0, lineToAdd);
34
- }
35
- return lines.join('\n');
36
- };
37
-
38
- export const modifyProjectBuildGradle = (gradle: string): string => {
39
- // Keep backward-compatible behavior: add supportLibVersion inside ext { } if missing
40
- if (!gradle.includes('supportLibVersion')) {
41
- const lines = gradle.split('\n');
42
- const extIndex = lines.findIndex((line) => line.trim() === 'ext {');
43
- if (extIndex !== -1) {
44
- lines.splice(extIndex + 1, 0, 'supportLibVersion = "28.0.0"');
45
- return lines.join('\n');
46
- }
47
- }
48
- return gradle;
49
- };
50
-
51
- const OPENIAP_GROUP = 'io.github.hyochan.openiap';
52
-
53
- const modifyAppBuildGradle = (
54
- gradle: string,
55
- isHorizonEnabled?: boolean,
56
- isFireOsEnabled?: boolean,
57
- ): string => {
58
- const escapeRegExp = (value: string): string => {
59
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
60
- };
61
-
62
- function loadOpenIapVersion(): string {
63
- try {
64
- const parsed = require('../../openiap-versions.json');
65
- const googleVersion =
66
- typeof parsed?.google === 'string' ? parsed.google.trim() : '';
67
- if (!googleVersion) {
68
- throw new Error(
69
- 'react-native-iap: "google" version missing or invalid in openiap-versions.json',
70
- );
71
- }
72
- return googleVersion;
73
- } catch (error) {
74
- throw new Error(
75
- `react-native-iap: Unable to load openiap-versions.json (${error instanceof Error ? error.message : error})`,
76
- );
77
- }
78
- }
79
-
80
- let modified = gradle;
81
-
82
- let openiapVersion: string;
83
- try {
84
- openiapVersion = loadOpenIapVersion();
85
- } catch (error) {
86
- WarningAggregator.addWarningAndroid(
87
- 'react-native-iap',
88
- `react-native-iap: Failed to resolve OpenIAP version (${error instanceof Error ? error.message : error})`,
89
- );
90
- return gradle;
91
- }
92
-
93
- // Replace legacy Billing/GMS instructions with OpenIAP Google library
94
- // Remove any old billingclient or play-services-base lines we may have added previously
95
- modified = modified
96
- .replace(
97
- /^[ \t]*(implementation|api)[ \t]+["']com\.android\.billingclient:billing-ktx:[^"']+["'][ \t]*$/gim,
98
- '',
99
- )
100
- .replace(
101
- /^[ \t]*(implementation|api)[ \t]+["']com\.google\.android\.gms:play-services-base:[^"']+["'][ \t]*$/gim,
102
- '',
103
- )
104
- .replace(/\n{3,}/g, '\n\n');
105
-
106
- const flavor = isFireOsEnabled
107
- ? 'amazon'
108
- : isHorizonEnabled
109
- ? 'horizon'
110
- : 'play';
111
- const artifactId = isFireOsEnabled
112
- ? 'openiap-google-amazon'
113
- : isHorizonEnabled
114
- ? 'openiap-google-horizon'
115
- : 'openiap-google';
116
- const openiapCoord = `${OPENIAP_GROUP}:${artifactId}`;
117
- const openiapDep = ` implementation "${openiapCoord}:${openiapVersion}"`;
118
- const desiredOpeniapLine = new RegExp(
119
- `^[ \\t]*(?:implementation|api)[ \\t]+\\(?["']${escapeRegExp(
120
- `${openiapCoord}:${openiapVersion}`,
121
- )}["']\\)?[ \\t]*$`,
122
- 'm',
123
- );
124
-
125
- const openiapAnyLine =
126
- /^[ \t]*(implementation|api)[ \t]+\(?["']io\.github\.hyochan\.openiap:openiap-google(?:-(?:horizon|amazon))?:[^"']+["']\)?[ \t]*$/gim;
127
- const withoutExistingOpeniap = modified.replace(openiapAnyLine, '');
128
- const hadExistingOpeniap = withoutExistingOpeniap !== modified;
129
- if (hadExistingOpeniap) {
130
- modified = withoutExistingOpeniap.replace(/\n{3,}/g, '\n\n');
131
- }
132
-
133
- if (!desiredOpeniapLine.test(modified)) {
134
- if (!/dependencies\s*{/.test(modified)) {
135
- modified += `\n\ndependencies {\n${openiapDep}\n}\n`;
136
- } else {
137
- modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep);
138
- }
139
- if (!hasLoggedPluginExecution) {
140
- console.log(
141
- `🛠️ react-native-iap: Added OpenIAP (${openiapVersion}) to build.gradle`,
142
- );
143
- }
144
- }
145
-
146
- // Remove stale OpenIAP platform strategies even when returning to the default
147
- // Play artifact. Otherwise a previous Fire OS/Horizon prebuild can keep
148
- // selecting the wrong local flavor.
149
- const strategyPattern =
150
- /^[ \t]*missingDimensionStrategy[ \t]*\(?[ \t]*["']platform["'][ \t]*,[ \t]*["'](play|horizon|amazon)["'][ \t]*\)?[ \t]*$/gm;
151
- const withoutExistingStrategy = modified.replace(strategyPattern, '');
152
- if (withoutExistingStrategy !== modified) {
153
- modified = withoutExistingStrategy;
154
- }
155
-
156
- const defaultConfigRegex = /defaultConfig\s*{/;
157
- if (defaultConfigRegex.test(modified)) {
158
- const strategyLine = ` missingDimensionStrategy "platform", "${flavor}"`;
159
- if (!/missingDimensionStrategy.*platform/.test(modified)) {
160
- modified = addLineToGradle(modified, defaultConfigRegex, strategyLine);
161
- }
162
- }
163
-
164
- return modified;
165
- };
166
-
167
- const withIapAndroid: ConfigPlugin<
168
- | {
169
- iapkitApiKey?: string;
170
- isHorizonEnabled?: boolean;
171
- isFireOsEnabled?: boolean;
172
- }
173
- | undefined
174
- > = (config, props) => {
175
- // Add OpenIAP dependency to app build.gradle
176
- config = withAppBuildGradle(config, (config) => {
177
- config.modResults.contents = modifyAppBuildGradle(
178
- config.modResults.contents,
179
- props?.isHorizonEnabled,
180
- props?.isFireOsEnabled,
181
- );
182
- return config;
183
- });
184
-
185
- config = withGradleProperties(config, (config) => {
186
- const horizonValue = props?.isHorizonEnabled ?? false;
187
- const fireOsValue = props?.isFireOsEnabled ?? false;
188
-
189
- config.modResults = config.modResults.filter(
190
- (item) =>
191
- item.type !== 'property' ||
192
- !['horizonEnabled', 'fireOsEnabled'].includes(item.key),
193
- );
194
-
195
- config.modResults.push({
196
- type: 'property',
197
- key: 'horizonEnabled',
198
- value: String(horizonValue),
199
- });
200
- config.modResults.push({
201
- type: 'property',
202
- key: 'fireOsEnabled',
203
- value: String(fireOsValue),
204
- });
205
-
206
- return config;
207
- });
208
-
209
- config = withAndroidManifest(config, (config) => {
210
- const manifest = config.modResults;
211
- const existingPermissions = manifest.manifest['uses-permission'];
212
- const permissions = Array.isArray(existingPermissions)
213
- ? existingPermissions
214
- : existingPermissions
215
- ? [existingPermissions]
216
- : [];
217
- manifest.manifest['uses-permission'] = permissions;
218
- const billingPerm = {$: {'android:name': 'com.android.vending.BILLING'}};
219
-
220
- if (props?.isFireOsEnabled) {
221
- manifest.manifest['uses-permission'] = permissions.filter(
222
- (p) => p.$['android:name'] !== 'com.android.vending.BILLING',
223
- );
224
- } else {
225
- const alreadyExists = permissions.some(
226
- (p) => p.$['android:name'] === 'com.android.vending.BILLING',
227
- );
228
- if (!alreadyExists) {
229
- permissions.push(billingPerm);
230
- if (!hasLoggedPluginExecution) {
231
- console.log(
232
- '✅ Added com.android.vending.BILLING to AndroidManifest.xml',
233
- );
234
- }
235
- } else {
236
- if (!hasLoggedPluginExecution) {
237
- console.log(
238
- 'ℹ️ com.android.vending.BILLING already exists in AndroidManifest.xml',
239
- );
240
- }
241
- }
242
- }
243
-
244
- // Add IAPKit API key as meta-data if provided
245
- if (props?.iapkitApiKey) {
246
- const application = manifest.manifest.application;
247
- const app = application?.[0];
248
- if (app) {
249
- if (!app['meta-data']) {
250
- app['meta-data'] = [];
251
- }
252
-
253
- const metaDataKey = 'dev.iapkit.API_KEY';
254
- const existingMetaData = (
255
- app['meta-data'] as {$?: {'android:name'?: string}}[]
256
- ).find((m) => m.$?.['android:name'] === metaDataKey);
257
-
258
- if (!existingMetaData) {
259
- app['meta-data'].push({
260
- $: {
261
- 'android:name': metaDataKey,
262
- 'android:value': props.iapkitApiKey,
263
- },
264
- });
265
- if (!hasLoggedPluginExecution) {
266
- console.log('✅ Added IAPKit API key to AndroidManifest.xml');
267
- }
268
- }
269
- }
270
- }
271
-
272
- return config;
273
- });
274
-
275
- return config;
276
- };
277
-
278
- export interface IosAlternativeBillingConfig {
279
- /** Country codes where external purchases are supported (ISO 3166-1 alpha-2) */
280
- countries?: string[];
281
- /** External purchase URLs per country (iOS 15.4+) */
282
- links?: Record<string, string>;
283
- /** Multiple external purchase URLs per country (iOS 17.5+, up to 5 per country) */
284
- multiLinks?: Record<string, string[]>;
285
- /** Custom link regions (iOS 18.1+) */
286
- customLinkRegions?: string[];
287
- /** Streaming link regions for music apps (iOS 18.2+) */
288
- streamingLinkRegions?: string[];
289
- /** Enable external purchase link entitlement */
290
- enableExternalPurchaseLink?: boolean;
291
- /** Enable external purchase link streaming entitlement (music apps only) */
292
- enableExternalPurchaseLinkStreaming?: boolean;
293
- }
294
-
295
- /** Add external purchase entitlements and Info.plist configuration */
296
- const withIosAlternativeBilling: ConfigPlugin<
297
- IosAlternativeBillingConfig | undefined
298
- > = (config, options) => {
299
- if (!options || !options.countries || options.countries.length === 0) {
300
- return config;
301
- }
302
-
303
- // Add entitlements
304
- config = withEntitlementsPlist(config, (config) => {
305
- // Always add basic external purchase entitlement when countries are specified
306
- config.modResults['com.apple.developer.storekit.external-purchase'] = true;
307
- if (!hasLoggedPluginExecution) {
308
- console.log(
309
- '✅ Added com.apple.developer.storekit.external-purchase to entitlements',
310
- );
311
- }
312
-
313
- // Add external purchase link entitlement if enabled
314
- if (options.enableExternalPurchaseLink) {
315
- config.modResults['com.apple.developer.storekit.external-purchase-link'] =
316
- true;
317
- if (!hasLoggedPluginExecution) {
318
- console.log(
319
- '✅ Added com.apple.developer.storekit.external-purchase-link to entitlements',
320
- );
321
- }
322
- }
323
-
324
- // Add streaming entitlement if enabled
325
- if (options.enableExternalPurchaseLinkStreaming) {
326
- config.modResults[
327
- 'com.apple.developer.storekit.external-purchase-link-streaming'
328
- ] = true;
329
- if (!hasLoggedPluginExecution) {
330
- console.log(
331
- '✅ Added com.apple.developer.storekit.external-purchase-link-streaming to entitlements',
332
- );
333
- }
334
- }
335
-
336
- return config;
337
- });
338
-
339
- // Add Info.plist configuration
340
- config = withInfoPlist(config, (config) => {
341
- const plist = config.modResults;
342
-
343
- // Helper function to normalize country codes to uppercase
344
- const normalize = (code: string) => code.trim().toUpperCase();
345
-
346
- // 1. SKExternalPurchase (Required)
347
- const normalizedCountries = options.countries?.map(normalize);
348
- plist.SKExternalPurchase = normalizedCountries;
349
- if (!hasLoggedPluginExecution) {
350
- console.log(
351
- `✅ Added SKExternalPurchase with countries: ${normalizedCountries?.join(
352
- ', ',
353
- )}`,
354
- );
355
- }
356
-
357
- // 2. SKExternalPurchaseLink (Optional - iOS 15.4+)
358
- if (options.links && Object.keys(options.links).length > 0) {
359
- plist.SKExternalPurchaseLink = Object.fromEntries(
360
- Object.entries(options.links).map(([code, url]) => [
361
- normalize(code),
362
- url,
363
- ]),
364
- );
365
- if (!hasLoggedPluginExecution) {
366
- console.log(
367
- `✅ Added SKExternalPurchaseLink for ${
368
- Object.keys(options.links).length
369
- } countries`,
370
- );
371
- }
372
- }
373
-
374
- // 3. SKExternalPurchaseMultiLink (iOS 17.5+)
375
- if (options.multiLinks && Object.keys(options.multiLinks).length > 0) {
376
- plist.SKExternalPurchaseMultiLink = Object.fromEntries(
377
- Object.entries(options.multiLinks).map(([code, urls]) => [
378
- normalize(code),
379
- urls,
380
- ]),
381
- );
382
- if (!hasLoggedPluginExecution) {
383
- console.log(
384
- `✅ Added SKExternalPurchaseMultiLink for ${
385
- Object.keys(options.multiLinks).length
386
- } countries`,
387
- );
388
- }
389
- }
390
-
391
- // 4. SKExternalPurchaseCustomLinkRegions (iOS 18.1+)
392
- if (options.customLinkRegions && options.customLinkRegions.length > 0) {
393
- plist.SKExternalPurchaseCustomLinkRegions =
394
- options.customLinkRegions.map(normalize);
395
- if (!hasLoggedPluginExecution) {
396
- console.log(
397
- `✅ Added SKExternalPurchaseCustomLinkRegions: ${options.customLinkRegions
398
- .map(normalize)
399
- .join(', ')}`,
400
- );
401
- }
402
- }
403
-
404
- // 5. SKExternalPurchaseLinkStreamingRegions (iOS 18.2+)
405
- if (
406
- options.streamingLinkRegions &&
407
- options.streamingLinkRegions.length > 0
408
- ) {
409
- plist.SKExternalPurchaseLinkStreamingRegions =
410
- options.streamingLinkRegions.map(normalize);
411
- if (!hasLoggedPluginExecution) {
412
- console.log(
413
- `✅ Added SKExternalPurchaseLinkStreamingRegions: ${options.streamingLinkRegions
414
- .map(normalize)
415
- .join(', ')}`,
416
- );
417
- }
418
- }
419
-
420
- return config;
421
- });
422
-
423
- return config;
424
- };
425
-
426
- type IapPluginProps = {
427
- ios?: {
428
- // Enable to inject Folly coroutine-disabling macros into Podfile during prebuild
429
- 'with-folly-no-coroutines'?: boolean;
430
- // @deprecated Use 'with-folly-no-coroutines'. Kept for backward compatibility.
431
- 'with-folly-no-couroutines'?: boolean;
432
- };
433
- /**
434
- * iOS Alternative Billing configuration.
435
- * Configure external purchase countries, links, and entitlements.
436
- * Requires approval from Apple.
437
- * @platform ios
438
- */
439
- iosAlternativeBilling?: IosAlternativeBillingConfig;
440
- /**
441
- * IAPKit project key for managed purchase verification.
442
- * This key will be added to AndroidManifest.xml (as meta-data) and Info.plist.
443
- * Get your project key from https://kit.openiap.dev
444
- */
445
- iapkitApiKey?: string;
446
- /**
447
- * Amazon platform targets.
448
- *
449
- * Fire OS selects the Android Amazon Appstore flavor. Vega OS is selected by
450
- * the Kepler runtime and does not change the Android Gradle flavor.
451
- */
452
- amazon?: {
453
- /**
454
- * Fire OS module for Amazon-distributed Android builds.
455
- * @platform android
456
- */
457
- fireOS?: boolean;
458
- /**
459
- * Vega OS runtime target. This is not an Android flavor.
460
- */
461
- vegaOS?: boolean;
462
- };
463
- /**
464
- * Optional non-Amazon Android store modules.
465
- * Fire OS takes precedence when both Fire OS and Horizon are enabled.
466
- */
467
- modules?: {
468
- /**
469
- * Horizon module for Meta Quest/VR devices.
470
- * @platform android
471
- */
472
- horizon?: boolean;
473
- };
474
- };
475
-
476
- export function resolveAmazonPlatformFlags(props?: IapPluginProps): {
477
- isFireOsEnabled: boolean;
478
- isVegaEnabled: boolean;
479
- isHorizonEnabled: boolean;
480
- } {
481
- const isFireOsEnabled = props?.amazon?.fireOS ?? false;
482
- const isVegaEnabled = props?.amazon?.vegaOS ?? false;
483
- const isHorizonEnabled = isFireOsEnabled
484
- ? false
485
- : (props?.modules?.horizon ?? false);
486
-
487
- return {
488
- isFireOsEnabled,
489
- isVegaEnabled,
490
- isHorizonEnabled,
491
- };
492
- }
493
-
494
- const withIapIosFollyWorkaround: ConfigPlugin<IapPluginProps | undefined> = (
495
- config,
496
- props,
497
- ) => {
498
- const newKey = props?.ios?.['with-folly-no-coroutines'];
499
- const oldKey = props?.ios?.['with-folly-no-couroutines'];
500
- if (oldKey && !hasLoggedPluginExecution) {
501
- // Temporary deprecation notice; remove when old key is dropped
502
- WarningAggregator.addWarningIOS(
503
- 'react-native-iap',
504
- "react-native-iap: 'ios.with-folly-no-couroutines' is deprecated; use 'ios.with-folly-no-coroutines'.",
505
- );
506
- }
507
- const enabled = !!(newKey ?? oldKey);
508
- if (!enabled) return config;
509
-
510
- return withPodfile(config, (config) => {
511
- let contents = config.modResults.contents;
512
-
513
- // Idempotency: if any of the defines already exists, assume it's applied
514
- if (
515
- contents.includes('FOLLY_CFG_NO_COROUTINES') ||
516
- contents.includes('FOLLY_HAS_COROUTINES=0')
517
- ) {
518
- return config;
519
- }
520
-
521
- const anchor = 'post_install do |installer|';
522
- const snippet = `
523
- # react-native-iap (expo): Disable Folly coroutines to avoid including non-vendored <folly/coro/*> headers
524
- installer.pods_project.targets.each do |target|
525
- target.build_configurations.each do |config|
526
- defs = (config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] || ['$(inherited)'])
527
- defs << 'FOLLY_NO_CONFIG=1' unless defs.any? { |d| d.to_s.include?('FOLLY_NO_CONFIG') }
528
- # Portability.h respects FOLLY_CFG_NO_COROUTINES to fully disable coroutine support
529
- defs << 'FOLLY_CFG_NO_COROUTINES=1' unless defs.any? { |d| d.to_s.include?('FOLLY_CFG_NO_COROUTINES') }
530
- defs << 'FOLLY_HAS_COROUTINES=0' unless defs.any? { |d| d.to_s.include?('FOLLY_HAS_COROUTINES') }
531
- config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = defs
532
- end
533
- end`;
534
-
535
- if (contents.includes(anchor)) {
536
- contents = contents.replace(anchor, `${anchor}\n${snippet}`);
537
- } else {
538
- // As a fallback, append a new post_install block
539
- contents += `
540
-
541
- ${anchor}
542
- ${snippet}
543
- end
544
- `;
545
- }
546
-
547
- config.modResults.contents = contents;
548
- return config;
549
- });
550
- };
551
-
552
- /** Add IAPKit API key to iOS Info.plist */
553
- const withIapkitApiKeyIOS: ConfigPlugin<string | undefined> = (
554
- config,
555
- apiKey,
556
- ) => {
557
- if (!apiKey) {
558
- return config;
559
- }
560
-
561
- return withInfoPlist(config, (config) => {
562
- const plist = config.modResults;
563
- const plistKey = 'IAPKitAPIKey';
564
-
565
- if (!plist[plistKey]) {
566
- plist[plistKey] = apiKey;
567
- if (!hasLoggedPluginExecution) {
568
- console.log('✅ Added IAPKit API key to Info.plist');
569
- }
570
- }
571
-
572
- return config;
573
- });
574
- };
575
-
576
- const withIAP: ConfigPlugin<IapPluginProps | undefined> = (config, props) => {
577
- try {
578
- const {isFireOsEnabled, isHorizonEnabled} =
579
- resolveAmazonPlatformFlags(props);
580
-
581
- let result = withIapAndroid(config, {
582
- iapkitApiKey: props?.iapkitApiKey,
583
- isHorizonEnabled,
584
- isFireOsEnabled,
585
- });
586
- result = withIapIosFollyWorkaround(result, props);
587
- // Add iOS alternative billing configuration if provided
588
- if (props?.iosAlternativeBilling) {
589
- result = withIosAlternativeBilling(result, props.iosAlternativeBilling);
590
- }
591
- // Add IAPKit API key to iOS Info.plist if provided
592
- if (props?.iapkitApiKey) {
593
- result = withIapkitApiKeyIOS(result, props.iapkitApiKey);
594
- }
595
- // Set flag after first execution to prevent duplicate logs
596
- hasLoggedPluginExecution = true;
597
- return result;
598
- } catch (error) {
599
- WarningAggregator.addWarningAndroid(
600
- 'react-native-iap',
601
- `react-native-iap plugin encountered an error: ${error}`,
602
- );
603
- console.error('react-native-iap plugin error:', error);
604
- return config;
605
- }
606
- };
607
-
608
- // Standard Expo config plugin export
609
- // Export a test-friendly wrapper that accepts 1 or 2 args
610
- type IapPluginCallable = {
611
- (config: ExpoConfig): ExpoConfig;
612
- (config: ExpoConfig, props?: IapPluginProps): ExpoConfig;
613
- };
614
-
615
- const _wrapped = createRunOncePlugin(
616
- withIAP,
617
- pkg.name,
618
- pkg.version,
619
- ) as unknown as (
620
- config: ExpoConfig,
621
- props: IapPluginProps | undefined,
622
- ) => ExpoConfig;
623
-
624
- const pluginExport: IapPluginCallable = ((
625
- config: ExpoConfig,
626
- props?: IapPluginProps,
627
- ) => _wrapped(config, props)) as unknown as IapPluginCallable;
628
-
629
- export {withIosAlternativeBilling};
630
- export {pluginExport as default};
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "commonjs",
5
- "lib": ["ES2020"],
6
- "declaration": true,
7
- "outDir": "./build",
8
- "rootDir": "./src",
9
- "strict": true,
10
- "esModuleInterop": true,
11
- "skipLibCheck": true,
12
- "forceConsistentCasingInFileNames": true,
13
- "resolveJsonModule": true,
14
- "moduleResolution": "node"
15
- },
16
- "include": ["src/**/*"],
17
- "exclude": ["node_modules", "build"]
18
- }