UNPKG

react-native-iap

Version:

React Native In-App Purchases module for iOS and Android using Nitro

271 lines (269 loc) â€ĸ 12.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.withIosAlternativeBilling = exports.modifyProjectBuildGradle = void 0; const config_plugins_1 = require("expo/config-plugins"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const pkg = require('../../package.json'); // Global flag to prevent duplicate logs let hasLoggedPluginExecution = false; const addLineToGradle = (content, anchor, lineToAdd, offset = 1) => { const lines = content.split('\n'); const index = lines.findIndex((line) => line.match(anchor)); if (index === -1) { console.warn(`Anchor "${anchor}" not found in build.gradle. Appending to end.`); lines.push(lineToAdd); } else { lines.splice(index + offset, 0, lineToAdd); } return lines.join('\n'); }; const modifyProjectBuildGradle = (gradle) => { // Keep backward-compatible behavior: add supportLibVersion inside ext { } if missing if (!gradle.includes('supportLibVersion')) { const lines = gradle.split('\n'); const extIndex = lines.findIndex((line) => line.trim() === 'ext {'); if (extIndex !== -1) { lines.splice(extIndex + 1, 0, 'supportLibVersion = "28.0.0"'); return lines.join('\n'); } } return gradle; }; exports.modifyProjectBuildGradle = modifyProjectBuildGradle; const OPENIAP_COORD = 'io.github.hyochan.openiap:openiap-google'; function loadOpenIapConfig() { const versionsPath = (0, node_path_1.resolve)(__dirname, '../../openiap-versions.json'); try { const raw = (0, node_fs_1.readFileSync)(versionsPath, 'utf8'); const parsed = JSON.parse(raw); const googleVersion = typeof parsed?.google === 'string' ? parsed.google.trim() : ''; if (!googleVersion) { throw new Error('react-native-iap: "google" version missing or invalid in openiap-versions.json'); } return { google: googleVersion }; } catch (error) { throw new Error(`react-native-iap: Unable to load openiap-versions.json (${error instanceof Error ? error.message : error})`); } } let cachedOpenIapVersion = null; const getOpenIapVersion = () => { if (cachedOpenIapVersion) { return cachedOpenIapVersion; } cachedOpenIapVersion = loadOpenIapConfig().google; return cachedOpenIapVersion; }; const modifyAppBuildGradle = (gradle) => { let modified = gradle; let openiapVersion; try { openiapVersion = getOpenIapVersion(); } catch (error) { config_plugins_1.WarningAggregator.addWarningAndroid('react-native-iap', `react-native-iap: Failed to resolve OpenIAP version (${error instanceof Error ? error.message : error})`); return gradle; } // Replace legacy Billing/GMS instructions with OpenIAP Google library // Remove any old billingclient or play-services-base lines we may have added previously modified = modified .replace(/^[ \t]*(implementation|api)[ \t]+["']com\.android\.billingclient:billing-ktx:[^"']+["'][ \t]*$/gim, '') .replace(/^[ \t]*(implementation|api)[ \t]+["']com\.google\.android\.gms:play-services-base:[^"']+["'][ \t]*$/gim, '') .replace(/\n{3,}/g, '\n\n'); const openiapDep = ` implementation "${OPENIAP_COORD}:${openiapVersion}"`; if (!modified.includes(OPENIAP_COORD)) { if (!/dependencies\s*{/.test(modified)) { modified += `\n\ndependencies {\n${openiapDep}\n}\n`; } else { modified = addLineToGradle(modified, /dependencies\s*{/, openiapDep); } if (!hasLoggedPluginExecution) { console.log(`đŸ› ī¸ react-native-iap: Added OpenIAP (${openiapVersion}) to build.gradle`); } } return modified; }; const withIapAndroid = (config) => { // Add OpenIAP dependency to app build.gradle config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => { config.modResults.contents = modifyAppBuildGradle(config.modResults.contents); return config; }); config = (0, config_plugins_1.withAndroidManifest)(config, (config) => { const manifest = config.modResults; if (!manifest.manifest['uses-permission']) { manifest.manifest['uses-permission'] = []; } const permissions = manifest.manifest['uses-permission']; const billingPerm = { $: { 'android:name': 'com.android.vending.BILLING' } }; const alreadyExists = permissions.some((p) => p.$['android:name'] === 'com.android.vending.BILLING'); if (!alreadyExists) { permissions.push(billingPerm); if (!hasLoggedPluginExecution) { console.log('✅ Added com.android.vending.BILLING to AndroidManifest.xml'); } } else { if (!hasLoggedPluginExecution) { console.log('â„šī¸ com.android.vending.BILLING already exists in AndroidManifest.xml'); } } return config; }); return config; }; /** Add external purchase entitlements and Info.plist configuration */ const withIosAlternativeBilling = (config, options) => { if (!options || !options.countries || options.countries.length === 0) { return config; } // Add entitlements config = (0, config_plugins_1.withEntitlementsPlist)(config, (config) => { // Always add basic external purchase entitlement when countries are specified config.modResults['com.apple.developer.storekit.external-purchase'] = true; if (!hasLoggedPluginExecution) { console.log('✅ Added com.apple.developer.storekit.external-purchase to entitlements'); } // Add external purchase link entitlement if enabled if (options.enableExternalPurchaseLink) { config.modResults['com.apple.developer.storekit.external-purchase-link'] = true; if (!hasLoggedPluginExecution) { console.log('✅ Added com.apple.developer.storekit.external-purchase-link to entitlements'); } } // Add streaming entitlement if enabled if (options.enableExternalPurchaseLinkStreaming) { config.modResults['com.apple.developer.storekit.external-purchase-link-streaming'] = true; if (!hasLoggedPluginExecution) { console.log('✅ Added com.apple.developer.storekit.external-purchase-link-streaming to entitlements'); } } return config; }); // Add Info.plist configuration config = (0, config_plugins_1.withInfoPlist)(config, (config) => { const plist = config.modResults; // Helper function to normalize country codes to uppercase const normalize = (code) => code.trim().toUpperCase(); // 1. SKExternalPurchase (Required) const normalizedCountries = options.countries?.map(normalize); plist.SKExternalPurchase = normalizedCountries; if (!hasLoggedPluginExecution) { console.log(`✅ Added SKExternalPurchase with countries: ${normalizedCountries?.join(', ')}`); } // 2. SKExternalPurchaseLink (Optional - iOS 15.4+) if (options.links && Object.keys(options.links).length > 0) { plist.SKExternalPurchaseLink = Object.fromEntries(Object.entries(options.links).map(([code, url]) => [ normalize(code), url, ])); if (!hasLoggedPluginExecution) { console.log(`✅ Added SKExternalPurchaseLink for ${Object.keys(options.links).length} countries`); } } // 3. SKExternalPurchaseMultiLink (iOS 17.5+) if (options.multiLinks && Object.keys(options.multiLinks).length > 0) { plist.SKExternalPurchaseMultiLink = Object.fromEntries(Object.entries(options.multiLinks).map(([code, urls]) => [ normalize(code), urls, ])); if (!hasLoggedPluginExecution) { console.log(`✅ Added SKExternalPurchaseMultiLink for ${Object.keys(options.multiLinks).length} countries`); } } // 4. SKExternalPurchaseCustomLinkRegions (iOS 18.1+) if (options.customLinkRegions && options.customLinkRegions.length > 0) { plist.SKExternalPurchaseCustomLinkRegions = options.customLinkRegions.map(normalize); if (!hasLoggedPluginExecution) { console.log(`✅ Added SKExternalPurchaseCustomLinkRegions: ${options.customLinkRegions .map(normalize) .join(', ')}`); } } // 5. SKExternalPurchaseLinkStreamingRegions (iOS 18.2+) if (options.streamingLinkRegions && options.streamingLinkRegions.length > 0) { plist.SKExternalPurchaseLinkStreamingRegions = options.streamingLinkRegions.map(normalize); if (!hasLoggedPluginExecution) { console.log(`✅ Added SKExternalPurchaseLinkStreamingRegions: ${options.streamingLinkRegions .map(normalize) .join(', ')}`); } } return config; }); return config; }; exports.withIosAlternativeBilling = withIosAlternativeBilling; const withIapIosFollyWorkaround = (config, props) => { const newKey = props?.ios?.['with-folly-no-coroutines']; const oldKey = props?.ios?.['with-folly-no-couroutines']; if (oldKey && !hasLoggedPluginExecution) { // Temporary deprecation notice; remove when old key is dropped config_plugins_1.WarningAggregator.addWarningIOS('react-native-iap', "react-native-iap: 'ios.with-folly-no-couroutines' is deprecated; use 'ios.with-folly-no-coroutines'."); } const enabled = !!(newKey ?? oldKey); if (!enabled) return config; return (0, config_plugins_1.withPodfile)(config, (config) => { let contents = config.modResults.contents; // Idempotency: if any of the defines already exists, assume it's applied if (contents.includes('FOLLY_CFG_NO_COROUTINES') || contents.includes('FOLLY_HAS_COROUTINES=0')) { return config; } const anchor = 'post_install do |installer|'; const snippet = ` # react-native-iap (expo): Disable Folly coroutines to avoid including non-vendored <folly/coro/*> headers installer.pods_project.targets.each do |target| target.build_configurations.each do |config| defs = (config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] || ['$(inherited)']) defs << 'FOLLY_NO_CONFIG=1' unless defs.any? { |d| d.to_s.include?('FOLLY_NO_CONFIG') } # Portability.h respects FOLLY_CFG_NO_COROUTINES to fully disable coroutine support defs << 'FOLLY_CFG_NO_COROUTINES=1' unless defs.any? { |d| d.to_s.include?('FOLLY_CFG_NO_COROUTINES') } defs << 'FOLLY_HAS_COROUTINES=0' unless defs.any? { |d| d.to_s.include?('FOLLY_HAS_COROUTINES') } config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = defs end end`; if (contents.includes(anchor)) { contents = contents.replace(anchor, `${anchor}\n${snippet}`); } else { // As a fallback, append a new post_install block contents += ` ${anchor} ${snippet} end `; } config.modResults.contents = contents; return config; }); }; const withIAP = (config, props) => { try { let result = withIapAndroid(config); result = withIapIosFollyWorkaround(result, props); // Add iOS alternative billing configuration if provided if (props?.iosAlternativeBilling) { result = withIosAlternativeBilling(result, props.iosAlternativeBilling); } // Set flag after first execution to prevent duplicate logs hasLoggedPluginExecution = true; return result; } catch (error) { config_plugins_1.WarningAggregator.addWarningAndroid('react-native-iap', `react-native-iap plugin encountered an error: ${error}`); console.error('react-native-iap plugin error:', error); return config; } }; const _wrapped = (0, config_plugins_1.createRunOncePlugin)(withIAP, pkg.name, pkg.version); const pluginExport = ((config, props) => _wrapped(config, props)); exports.default = pluginExport;