From 34c8e6ae94e1a1f97bb777b686c80ff4040fd474 Mon Sep 17 00:00:00 2001 From: retweakr Date: Mon, 16 Mar 2026 04:11:59 +0400 Subject: [PATCH 1/6] fix(ios): Add OnsideKit as a default dependency in ExpoIap.podspec --- ios/ExpoIap.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/ExpoIap.podspec b/ios/ExpoIap.podspec index eb1fb0578..480ae268e 100644 --- a/ios/ExpoIap.podspec +++ b/ios/ExpoIap.podspec @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.dependency 'ExpoModulesCore' s.dependency 'openiap', "#{versions['apple']}" - # OnsideKit is optional; added via ensureOnsidePod() in Podfile when modules.onside is enabled + s.dependency 'OnsideKit' # Swift/Objective-C compatibility s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', From b1f7ccce9a32bd69272a7be464992b798b622690 Mon Sep 17 00:00:00 2001 From: retweakr Date: Mon, 16 Mar 2026 18:36:13 +0400 Subject: [PATCH 2/6] refactor(ios): Move OnsideKit to a subspec in ExpoIap.podspec and update Podfile handling --- ios/ExpoIap.podspec | 5 ++++- plugin/src/withIAP.ts | 26 ++++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/ios/ExpoIap.podspec b/ios/ExpoIap.podspec index 480ae268e..d98e5fb09 100644 --- a/ios/ExpoIap.podspec +++ b/ios/ExpoIap.podspec @@ -26,7 +26,10 @@ Pod::Spec.new do |s| s.dependency 'ExpoModulesCore' s.dependency 'openiap', "#{versions['apple']}" - s.dependency 'OnsideKit' + s.subspec 'Onside' do |ss| + ss.dependency 'OnsideKit' + end + # Swift/Objective-C compatibility s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', diff --git a/plugin/src/withIAP.ts b/plugin/src/withIAP.ts index c52df44dd..838394f1f 100644 --- a/plugin/src/withIAP.ts +++ b/plugin/src/withIAP.ts @@ -263,12 +263,16 @@ const withIapAndroid: ConfigPlugin< return config; }; -const ensureOnsidePod = (content: string): string => { - const podLine = - " pod 'OnsideKit', :podspec => 'https://raw.githubusercontent.com/onside-io/OnsideKit-iOS/0.5.0/OnsideKit.podspec'"; - const podRegex = /^\s*pod\s+'OnsideKit'\b.*$/m; +const ONSIDEKIT_PODSPEC_URL = + 'https://raw.githubusercontent.com/onside-io/OnsideKit-iOS/0.5.0/OnsideKit.podspec'; + +const EXPO_IAP_IOS_PATH = '../node_modules/expo-iap/ios'; - if (podRegex.test(content)) { +const ensureOnsidePod = (content: string): string => { + const alreadyHasOnside = + /^\s*pod\s+['"]ExpoIap\/Onside['"].*$/m.test(content) || + /^\s*pod\s+['"]OnsideKit['"]\b.*$/m.test(content); + if (alreadyHasOnside) { return content; } @@ -276,7 +280,7 @@ const ensureOnsidePod = (content: string): string => { if (!targetMatch) { WarningAggregator.addWarningIOS( 'expo-iap', - 'Could not find a target block in Podfile when adding OnsideKit; skipping installation.', + 'Could not find a target block in Podfile when adding ExpoIap/Onside; skipping installation.', ); return content; } @@ -285,9 +289,15 @@ const ensureOnsidePod = (content: string): string => { const before = content.slice(0, insertIndex); const after = content.slice(insertIndex); - logOnce('๐Ÿ“ฆ expo-iap: Added OnsideKit pod to Podfile'); + const podLines = + ` pod 'OnsideKit', :podspec => '${ONSIDEKIT_PODSPEC_URL}'\n` + + ` pod 'ExpoIap/Onside', :path => '${EXPO_IAP_IOS_PATH}'`; + + logOnce( + '๐Ÿ“ฆ expo-iap: Added ExpoIap/Onside subspec (and OnsideKit for resolution) to Podfile', + ); - return `${before}${podLine}\n${after}`; + return `${before}${podLines}\n${after}`; }; export type AutolinkState = {expoIap: boolean; onside: boolean}; From 6b49a29470a0ef638e75dd6e4986eb4a47a498fc Mon Sep 17 00:00:00 2001 From: retweakr Date: Tue, 17 Mar 2026 14:55:33 +0400 Subject: [PATCH 3/6] fix(ios): Adjust OnsideKit and ExpoIap/Onside handling in Podfile logic and podspec --- ios/ExpoIap.podspec | 1 + plugin/src/withIAP.ts | 40 ++++++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/ios/ExpoIap.podspec b/ios/ExpoIap.podspec index d98e5fb09..711539e5a 100644 --- a/ios/ExpoIap.podspec +++ b/ios/ExpoIap.podspec @@ -37,4 +37,5 @@ Pod::Spec.new do |s| } s.source_files = "**/*.{h,m,swift}" + s.default_subspecs = [] end diff --git a/plugin/src/withIAP.ts b/plugin/src/withIAP.ts index 838394f1f..97096825a 100644 --- a/plugin/src/withIAP.ts +++ b/plugin/src/withIAP.ts @@ -268,11 +268,21 @@ const ONSIDEKIT_PODSPEC_URL = const EXPO_IAP_IOS_PATH = '../node_modules/expo-iap/ios'; -const ensureOnsidePod = (content: string): string => { - const alreadyHasOnside = - /^\s*pod\s+['"]ExpoIap\/Onside['"].*$/m.test(content) || - /^\s*pod\s+['"]OnsideKit['"]\b.*$/m.test(content); - if (alreadyHasOnside) { +const ensureOnsidePod = (content: string, onside: boolean): string => { + const alreadyHasOnsideKit = /^\s*pod\s+['"]OnsideKit['"]\b.*$/m.test(content); + const alreadyHasExpoIapOnside = /^\s*pod\s+['"]ExpoIap\/Onside['"].*$/m.test( + content, + ); + + let podLines = ''; + if (!alreadyHasOnsideKit) { + podLines += ` pod 'OnsideKit', :podspec => '${ONSIDEKIT_PODSPEC_URL}'\n`; + } + if (onside && !alreadyHasExpoIapOnside) { + podLines += ` pod 'ExpoIap/Onside', :path => '${EXPO_IAP_IOS_PATH}'`; + } + + if (!podLines) { return content; } @@ -289,13 +299,13 @@ const ensureOnsidePod = (content: string): string => { const before = content.slice(0, insertIndex); const after = content.slice(insertIndex); - const podLines = - ` pod 'OnsideKit', :podspec => '${ONSIDEKIT_PODSPEC_URL}'\n` + - ` pod 'ExpoIap/Onside', :path => '${EXPO_IAP_IOS_PATH}'`; - - logOnce( - '๐Ÿ“ฆ expo-iap: Added ExpoIap/Onside subspec (and OnsideKit for resolution) to Podfile', - ); + if (onside && !alreadyHasExpoIapOnside) { + logOnce( + '๐Ÿ“ฆ expo-iap: Added ExpoIap/Onside subspec (and OnsideKit for resolution) to Podfile', + ); + } else if (!alreadyHasOnsideKit) { + logOnce('๐Ÿ“ฆ expo-iap: Added OnsideKit to Podfile'); + } return `${before}${podLines}\n${after}`; }; @@ -501,10 +511,8 @@ const withIapIOS: ConfigPlugin = ( logOnce('๐Ÿงน expo-iap: Removed local OpenIAP pod from Podfile'); } - // 3) Optionally install OnsideKit when enabled in config - if (options?.enableOnside) { - content = ensureOnsidePod(content); - } + // 3) Always add OnsideKit; add ExpoIap/Onside only when onside is enabled + content = ensureOnsidePod(content, options?.enableOnside ?? false); config.modResults.contents = content; return config; From 498680cf3226de7625f68a2e4e88fb69c490c39c Mon Sep 17 00:00:00 2001 From: hyochan Date: Tue, 17 Mar 2026 23:47:15 +0900 Subject: [PATCH 4/6] fix(ios): use subspec for conditional OnsideKit dependency - Add ExpoIap/Onside subspec with default_subspecs = [] so OnsideKit is only included when explicitly enabled - Rename ensureOnsidePod to ensureOnsidePodIOS per naming convention - Keep call site conditional: only called when enableOnside is true - Add ensureOnsidePodIOS tests covering onside true/false scenarios Co-Authored-By: Claude Opus 4.6 (1M context) --- ios/ExpoIap.podspec | 3 +- plugin/__tests__/withIAP.test.ts | 95 +++++++++++++++++++++++++++++++- plugin/src/withIAP.ts | 39 ++++++------- 3 files changed, 113 insertions(+), 24 deletions(-) diff --git a/ios/ExpoIap.podspec b/ios/ExpoIap.podspec index 711539e5a..56b831af1 100644 --- a/ios/ExpoIap.podspec +++ b/ios/ExpoIap.podspec @@ -30,6 +30,8 @@ Pod::Spec.new do |s| ss.dependency 'OnsideKit' end + s.default_subspecs = [] + # Swift/Objective-C compatibility s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', @@ -37,5 +39,4 @@ Pod::Spec.new do |s| } s.source_files = "**/*.{h,m,swift}" - s.default_subspecs = [] end diff --git a/plugin/__tests__/withIAP.test.ts b/plugin/__tests__/withIAP.test.ts index 976885164..0e3eaf286 100644 --- a/plugin/__tests__/withIAP.test.ts +++ b/plugin/__tests__/withIAP.test.ts @@ -1,6 +1,7 @@ import type {ExpoConfig} from '@expo/config-types'; import { computeAutolinkModules, + ensureOnsidePodIOS, modifyAppBuildGradle, resolveModuleSelection, } from '../src/withIAP'; @@ -28,7 +29,7 @@ jest.mock('expo/config-plugins', () => { return { ...plugins, - WarningAggregator: {addWarningAndroid: jest.fn()}, + WarningAggregator: {addWarningAndroid: jest.fn(), addWarningIOS: jest.fn()}, }; }); @@ -183,3 +184,95 @@ describe('ios module selection', () => { }); }); }); + +describe('ensureOnsidePodIOS', () => { + const basePodfile = [ + "source 'https://cdn.cocoapods.org/'", + '', + "target 'MyApp' do", + " pod 'ExpoModulesCore'", + 'end', + '', + ].join('\n'); + + it('adds both OnsideKit and ExpoIap/Onside pods', () => { + const result = ensureOnsidePodIOS(basePodfile); + expect(result).toContain("pod 'OnsideKit'"); + expect(result).toContain("pod 'ExpoIap/Onside'"); + }); + + it('inserts pods inside the target block', () => { + const result = ensureOnsidePodIOS(basePodfile); + const targetIndex = result.indexOf("target 'MyApp' do"); + const onsideKitIndex = result.indexOf("pod 'OnsideKit'"); + const endIndex = result.indexOf('end'); + expect(onsideKitIndex).toBeGreaterThan(targetIndex); + expect(onsideKitIndex).toBeLessThan(endIndex); + }); + + it('skips if both pods already exist', () => { + const podfileWithBoth = [ + "target 'MyApp' do", + " pod 'OnsideKit', :podspec => 'https://example.com'", + " pod 'ExpoIap/Onside', :path => '../node_modules/expo-iap/ios'", + 'end', + ].join('\n'); + const result = ensureOnsidePodIOS(podfileWithBoth); + expect(result).toBe(podfileWithBoth); + }); + + it('adds missing ExpoIap/Onside when OnsideKit already exists', () => { + const podfileWithOnsideKit = [ + "target 'MyApp' do", + " pod 'OnsideKit', :podspec => 'https://example.com'", + 'end', + ].join('\n'); + const result = ensureOnsidePodIOS(podfileWithOnsideKit); + expect(result).toContain("pod 'ExpoIap/Onside'"); + expect(result).not.toContain('raw.githubusercontent'); + }); + + it('adds missing OnsideKit when ExpoIap/Onside already exists', () => { + const podfileWithSubspec = [ + "target 'MyApp' do", + " pod 'ExpoIap/Onside', :path => '../node_modules/expo-iap/ios'", + 'end', + ].join('\n'); + const result = ensureOnsidePodIOS(podfileWithSubspec); + expect(result).toContain("pod 'OnsideKit'"); + const subspecCount = (result.match(/pod 'ExpoIap\/Onside'/g) ?? []).length; + expect(subspecCount).toBe(1); + }); + + it('returns unchanged content when no target block found', () => { + const noPodfile = '# empty'; + const result = ensureOnsidePodIOS(noPodfile); + expect(result).toBe(noPodfile); + }); + + it('does not modify Podfile when onside is disabled (not called)', () => { + const enableOnside = false; + let content = basePodfile; + + if (enableOnside) { + content = ensureOnsidePodIOS(content); + } + + expect(content).toBe(basePodfile); + expect(content).not.toContain("pod 'OnsideKit'"); + expect(content).not.toContain("pod 'ExpoIap/Onside'"); + }); + + it('modifies Podfile when onside is enabled', () => { + const enableOnside = true; + let content = basePodfile; + + if (enableOnside) { + content = ensureOnsidePodIOS(content); + } + + expect(content).not.toBe(basePodfile); + expect(content).toContain("pod 'OnsideKit'"); + expect(content).toContain("pod 'ExpoIap/Onside'"); + }); +}); diff --git a/plugin/src/withIAP.ts b/plugin/src/withIAP.ts index 97096825a..399a6049f 100644 --- a/plugin/src/withIAP.ts +++ b/plugin/src/withIAP.ts @@ -7,7 +7,6 @@ import { withGradleProperties, withInfoPlist, withPodfile, - withAppDelegate, } from 'expo/config-plugins'; import type {ExpoConfig} from '@expo/config-types'; import * as fs from 'fs'; @@ -268,21 +267,13 @@ const ONSIDEKIT_PODSPEC_URL = const EXPO_IAP_IOS_PATH = '../node_modules/expo-iap/ios'; -const ensureOnsidePod = (content: string, onside: boolean): string => { - const alreadyHasOnsideKit = /^\s*pod\s+['"]OnsideKit['"]\b.*$/m.test(content); +export const ensureOnsidePodIOS = (content: string): string => { + const alreadyHasOnsideKit = /^\s*pod\s+['"]OnsideKit['"].*$/m.test(content); const alreadyHasExpoIapOnside = /^\s*pod\s+['"]ExpoIap\/Onside['"].*$/m.test( content, ); - let podLines = ''; - if (!alreadyHasOnsideKit) { - podLines += ` pod 'OnsideKit', :podspec => '${ONSIDEKIT_PODSPEC_URL}'\n`; - } - if (onside && !alreadyHasExpoIapOnside) { - podLines += ` pod 'ExpoIap/Onside', :path => '${EXPO_IAP_IOS_PATH}'`; - } - - if (!podLines) { + if (alreadyHasOnsideKit && alreadyHasExpoIapOnside) { return content; } @@ -295,19 +286,21 @@ const ensureOnsidePod = (content: string, onside: boolean): string => { return content; } + let podLines = ''; + if (!alreadyHasOnsideKit) { + podLines += ` pod 'OnsideKit', :podspec => '${ONSIDEKIT_PODSPEC_URL}'\n`; + } + if (!alreadyHasExpoIapOnside) { + podLines += ` pod 'ExpoIap/Onside', :path => '${EXPO_IAP_IOS_PATH}'\n`; + } + const insertIndex = targetMatch.index! + targetMatch[0].length; const before = content.slice(0, insertIndex); const after = content.slice(insertIndex); - if (onside && !alreadyHasExpoIapOnside) { - logOnce( - '๐Ÿ“ฆ expo-iap: Added ExpoIap/Onside subspec (and OnsideKit for resolution) to Podfile', - ); - } else if (!alreadyHasOnsideKit) { - logOnce('๐Ÿ“ฆ expo-iap: Added OnsideKit to Podfile'); - } + logOnce('๐Ÿ“ฆ expo-iap: Added ExpoIap/Onside subspec to Podfile'); - return `${before}${podLines}\n${after}`; + return `${before}${podLines}${after}`; }; export type AutolinkState = {expoIap: boolean; onside: boolean}; @@ -511,8 +504,10 @@ const withIapIOS: ConfigPlugin = ( logOnce('๐Ÿงน expo-iap: Removed local OpenIAP pod from Podfile'); } - // 3) Always add OnsideKit; add ExpoIap/Onside only when onside is enabled - content = ensureOnsidePod(content, options?.enableOnside ?? false); + // 3) Optionally install OnsideKit when enabled in config + if (options?.enableOnside) { + content = ensureOnsidePodIOS(content); + } config.modResults.contents = content; return config; From 15fa2b51d1e0dd33ee2ac8b004f9d63182819a00 Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 18 Mar 2026 00:07:01 +0900 Subject: [PATCH 5/6] fix(ios): use post_install hook for OnsideKit visibility instead of subspec OnsideKit is not on CocoaPods CDN yet, so subspec dependency fails to resolve. Instead, inject a post_install hook that adds OnsideKit's build dir to ExpoIap's SWIFT_INCLUDE_PATHS, making #if canImport(OnsideKit) work without requiring a podspec dependency. Co-Authored-By: Claude Opus 4.6 (1M context) --- ios/ExpoIap.podspec | 8 ++-- plugin/__tests__/withIAP.test.ts | 62 ++++++++++++++++---------- plugin/src/withIAP.ts | 76 ++++++++++++++++++++++---------- 3 files changed, 95 insertions(+), 51 deletions(-) diff --git a/ios/ExpoIap.podspec b/ios/ExpoIap.podspec index 56b831af1..c34974f06 100644 --- a/ios/ExpoIap.podspec +++ b/ios/ExpoIap.podspec @@ -26,11 +26,9 @@ Pod::Spec.new do |s| s.dependency 'ExpoModulesCore' s.dependency 'openiap', "#{versions['apple']}" - s.subspec 'Onside' do |ss| - ss.dependency 'OnsideKit' - end - - s.default_subspecs = [] + # OnsideKit is optional; added via ensureOnsidePodIOS() in Podfile when modules.onside is enabled + # A post_install hook makes OnsideKit visible to ExpoIap so #if canImport(OnsideKit) works. + # Once OnsideKit is published to CocoaPods CDN, this can be replaced with a subspec dependency. # Swift/Objective-C compatibility s.pod_target_xcconfig = { diff --git a/plugin/__tests__/withIAP.test.ts b/plugin/__tests__/withIAP.test.ts index 0e3eaf286..7cd824ad0 100644 --- a/plugin/__tests__/withIAP.test.ts +++ b/plugin/__tests__/withIAP.test.ts @@ -195,13 +195,16 @@ describe('ensureOnsidePodIOS', () => { '', ].join('\n'); - it('adds both OnsideKit and ExpoIap/Onside pods', () => { + it('adds OnsideKit pod and post_install hook', () => { const result = ensureOnsidePodIOS(basePodfile); expect(result).toContain("pod 'OnsideKit'"); - expect(result).toContain("pod 'ExpoIap/Onside'"); + expect(result).toContain('# [expo-iap] Make OnsideKit visible'); + expect(result).toContain('post_install do |installer|'); + expect(result).toContain("target.name == 'ExpoIap'"); + expect(result).toContain('SWIFT_INCLUDE_PATHS'); }); - it('inserts pods inside the target block', () => { + it('inserts OnsideKit pod inside the target block', () => { const result = ensureOnsidePodIOS(basePodfile); const targetIndex = result.indexOf("target 'MyApp' do"); const onsideKitIndex = result.indexOf("pod 'OnsideKit'"); @@ -210,38 +213,51 @@ describe('ensureOnsidePodIOS', () => { expect(onsideKitIndex).toBeLessThan(endIndex); }); - it('skips if both pods already exist', () => { - const podfileWithBoth = [ + it('appends into existing post_install block', () => { + const podfileWithPostInstall = [ "target 'MyApp' do", - " pod 'OnsideKit', :podspec => 'https://example.com'", - " pod 'ExpoIap/Onside', :path => '../node_modules/expo-iap/ios'", + " pod 'ExpoModulesCore'", + 'end', + '', + 'post_install do |installer|', + ' # existing hook', 'end', ].join('\n'); - const result = ensureOnsidePodIOS(podfileWithBoth); - expect(result).toBe(podfileWithBoth); + const result = ensureOnsidePodIOS(podfileWithPostInstall); + expect(result).toContain("pod 'OnsideKit'"); + expect(result).toContain('# [expo-iap] Make OnsideKit visible'); + // Should not create a second post_install block + const postInstallCount = ( + result.match(/post_install do \|installer\|/g) ?? [] + ).length; + expect(postInstallCount).toBe(1); }); - it('adds missing ExpoIap/Onside when OnsideKit already exists', () => { - const podfileWithOnsideKit = [ + it('skips if OnsideKit and post_install hook already exist', () => { + const podfileComplete = [ "target 'MyApp' do", " pod 'OnsideKit', :podspec => 'https://example.com'", 'end', + '', + 'post_install do |installer|', + ' # [expo-iap] Make OnsideKit visible', + 'end', ].join('\n'); - const result = ensureOnsidePodIOS(podfileWithOnsideKit); - expect(result).toContain("pod 'ExpoIap/Onside'"); - expect(result).not.toContain('raw.githubusercontent'); + const result = ensureOnsidePodIOS(podfileComplete); + expect(result).toBe(podfileComplete); }); - it('adds missing OnsideKit when ExpoIap/Onside already exists', () => { - const podfileWithSubspec = [ + it('adds post_install hook when OnsideKit already exists', () => { + const podfileWithOnsideKit = [ "target 'MyApp' do", - " pod 'ExpoIap/Onside', :path => '../node_modules/expo-iap/ios'", + " pod 'OnsideKit', :podspec => 'https://example.com'", 'end', ].join('\n'); - const result = ensureOnsidePodIOS(podfileWithSubspec); - expect(result).toContain("pod 'OnsideKit'"); - const subspecCount = (result.match(/pod 'ExpoIap\/Onside'/g) ?? []).length; - expect(subspecCount).toBe(1); + const result = ensureOnsidePodIOS(podfileWithOnsideKit); + expect(result).toContain('# [expo-iap] Make OnsideKit visible'); + // Should not add a duplicate OnsideKit pod + const onsideKitCount = (result.match(/pod 'OnsideKit'/g) ?? []).length; + expect(onsideKitCount).toBe(1); }); it('returns unchanged content when no target block found', () => { @@ -260,7 +276,7 @@ describe('ensureOnsidePodIOS', () => { expect(content).toBe(basePodfile); expect(content).not.toContain("pod 'OnsideKit'"); - expect(content).not.toContain("pod 'ExpoIap/Onside'"); + expect(content).not.toContain('# [expo-iap] Make OnsideKit visible'); }); it('modifies Podfile when onside is enabled', () => { @@ -273,6 +289,6 @@ describe('ensureOnsidePodIOS', () => { expect(content).not.toBe(basePodfile); expect(content).toContain("pod 'OnsideKit'"); - expect(content).toContain("pod 'ExpoIap/Onside'"); + expect(content).toContain('# [expo-iap] Make OnsideKit visible'); }); }); diff --git a/plugin/src/withIAP.ts b/plugin/src/withIAP.ts index 399a6049f..e5a8c1342 100644 --- a/plugin/src/withIAP.ts +++ b/plugin/src/withIAP.ts @@ -265,42 +265,72 @@ const withIapAndroid: ConfigPlugin< const ONSIDEKIT_PODSPEC_URL = 'https://raw.githubusercontent.com/onside-io/OnsideKit-iOS/0.5.0/OnsideKit.podspec'; -const EXPO_IAP_IOS_PATH = '../node_modules/expo-iap/ios'; +// post_install hook that adds OnsideKit's module to ExpoIap's search paths +// so that `#if canImport(OnsideKit)` resolves correctly at build time. +// This is needed because OnsideKit is not yet on CocoaPods CDN, so we cannot +// use a podspec `s.dependency`. Once OnsideKit is on CDN, replace with a subspec. +const ONSIDE_POST_INSTALL_HOOK = ` + # [expo-iap] Make OnsideKit visible to ExpoIap for #if canImport(OnsideKit) + installer.pods_project.targets.each do |target| + if target.name == 'ExpoIap' + target.build_configurations.each do |config| + swift_paths = config.build_settings['SWIFT_INCLUDE_PATHS'] || '$(inherited)' + onside_path = '\${PODS_CONFIGURATION_BUILD_DIR}/OnsideKit' + unless swift_paths.include?(onside_path) + config.build_settings['SWIFT_INCLUDE_PATHS'] = "#{swift_paths} #{onside_path}" + end + end + end + end`; + +const ONSIDE_POST_INSTALL_MARKER = '# [expo-iap] Make OnsideKit visible'; export const ensureOnsidePodIOS = (content: string): string => { const alreadyHasOnsideKit = /^\s*pod\s+['"]OnsideKit['"].*$/m.test(content); - const alreadyHasExpoIapOnside = /^\s*pod\s+['"]ExpoIap\/Onside['"].*$/m.test( - content, - ); + const alreadyHasPostInstall = content.includes(ONSIDE_POST_INSTALL_MARKER); - if (alreadyHasOnsideKit && alreadyHasExpoIapOnside) { + if (alreadyHasOnsideKit && alreadyHasPostInstall) { return content; } - const targetMatch = content.match(/target\s+'[^']+'\s+do\s*\n/); - if (!targetMatch) { - WarningAggregator.addWarningIOS( - 'expo-iap', - 'Could not find a target block in Podfile when adding ExpoIap/Onside; skipping installation.', - ); - return content; - } + let result = content; - let podLines = ''; + // 1) Add OnsideKit pod inside the target block if (!alreadyHasOnsideKit) { - podLines += ` pod 'OnsideKit', :podspec => '${ONSIDEKIT_PODSPEC_URL}'\n`; - } - if (!alreadyHasExpoIapOnside) { - podLines += ` pod 'ExpoIap/Onside', :path => '${EXPO_IAP_IOS_PATH}'\n`; + const targetMatch = result.match(/target\s+'[^']+'\s+do\s*\n/); + if (!targetMatch) { + WarningAggregator.addWarningIOS( + 'expo-iap', + 'Could not find a target block in Podfile when adding OnsideKit; skipping installation.', + ); + return content; + } + + const podLine = ` pod 'OnsideKit', :podspec => '${ONSIDEKIT_PODSPEC_URL}'\n`; + const insertIndex = targetMatch.index! + targetMatch[0].length; + result = result.slice(0, insertIndex) + podLine + result.slice(insertIndex); } - const insertIndex = targetMatch.index! + targetMatch[0].length; - const before = content.slice(0, insertIndex); - const after = content.slice(insertIndex); + // 2) Add post_install hook to make OnsideKit visible to ExpoIap + if (!alreadyHasPostInstall) { + const postInstallMatch = result.match(/post_install\s+do\s+\|installer\|/); + if (postInstallMatch) { + // Append inside existing post_install block + const insertIndex = postInstallMatch.index! + postInstallMatch[0].length; + result = + result.slice(0, insertIndex) + + '\n' + + ONSIDE_POST_INSTALL_HOOK + + result.slice(insertIndex); + } else { + // Create new post_install block + result += `\npost_install do |installer|${ONSIDE_POST_INSTALL_HOOK}\nend\n`; + } + } - logOnce('๐Ÿ“ฆ expo-iap: Added ExpoIap/Onside subspec to Podfile'); + logOnce('๐Ÿ“ฆ expo-iap: Added OnsideKit pod and post_install hook to Podfile'); - return `${before}${podLines}${after}`; + return result; }; export type AutolinkState = {expoIap: boolean; onside: boolean}; From 0057e54230734bce95ede085a9df5c5edf061137 Mon Sep 17 00:00:00 2001 From: hyochan Date: Wed, 18 Mar 2026 00:40:18 +0900 Subject: [PATCH 6/6] fix(ios): simplify OnsideKit integration with subspec approach Replace post_install hook workaround with a clean subspec dependency. The plugin adds `pod 'ExpoIap/Onside'` to Podfile only when onside is enabled. Requires OnsideKit to be published on CocoaPods CDN. Co-Authored-By: Claude Opus 4.6 (1M context) --- ios/ExpoIap.podspec | 10 +++-- plugin/__tests__/withIAP.test.ts | 69 ++++++------------------------ plugin/src/withIAP.ts | 73 ++++++-------------------------- 3 files changed, 33 insertions(+), 119 deletions(-) diff --git a/ios/ExpoIap.podspec b/ios/ExpoIap.podspec index c34974f06..fcf599f54 100644 --- a/ios/ExpoIap.podspec +++ b/ios/ExpoIap.podspec @@ -26,9 +26,13 @@ Pod::Spec.new do |s| s.dependency 'ExpoModulesCore' s.dependency 'openiap', "#{versions['apple']}" - # OnsideKit is optional; added via ensureOnsidePodIOS() in Podfile when modules.onside is enabled - # A post_install hook makes OnsideKit visible to ExpoIap so #if canImport(OnsideKit) works. - # Once OnsideKit is published to CocoaPods CDN, this can be replaced with a subspec dependency. + # OnsideKit is optional; only included when modules.onside is enabled via the Expo plugin. + # The plugin adds `pod 'ExpoIap/Onside'` to the Podfile when onside is enabled. + s.subspec 'Onside' do |ss| + ss.dependency 'OnsideKit' + end + + s.default_subspecs = [] # Swift/Objective-C compatibility s.pod_target_xcconfig = { diff --git a/plugin/__tests__/withIAP.test.ts b/plugin/__tests__/withIAP.test.ts index 7cd824ad0..f903540b0 100644 --- a/plugin/__tests__/withIAP.test.ts +++ b/plugin/__tests__/withIAP.test.ts @@ -195,69 +195,28 @@ describe('ensureOnsidePodIOS', () => { '', ].join('\n'); - it('adds OnsideKit pod and post_install hook', () => { + it('adds ExpoIap/Onside subspec pod', () => { const result = ensureOnsidePodIOS(basePodfile); - expect(result).toContain("pod 'OnsideKit'"); - expect(result).toContain('# [expo-iap] Make OnsideKit visible'); - expect(result).toContain('post_install do |installer|'); - expect(result).toContain("target.name == 'ExpoIap'"); - expect(result).toContain('SWIFT_INCLUDE_PATHS'); + expect(result).toContain("pod 'ExpoIap/Onside'"); }); - it('inserts OnsideKit pod inside the target block', () => { + it('inserts pod inside the target block', () => { const result = ensureOnsidePodIOS(basePodfile); const targetIndex = result.indexOf("target 'MyApp' do"); - const onsideKitIndex = result.indexOf("pod 'OnsideKit'"); + const subspecIndex = result.indexOf("pod 'ExpoIap/Onside'"); const endIndex = result.indexOf('end'); - expect(onsideKitIndex).toBeGreaterThan(targetIndex); - expect(onsideKitIndex).toBeLessThan(endIndex); + expect(subspecIndex).toBeGreaterThan(targetIndex); + expect(subspecIndex).toBeLessThan(endIndex); }); - it('appends into existing post_install block', () => { - const podfileWithPostInstall = [ + it('skips if ExpoIap/Onside already exists', () => { + const podfileWithSubspec = [ "target 'MyApp' do", - " pod 'ExpoModulesCore'", - 'end', - '', - 'post_install do |installer|', - ' # existing hook', - 'end', - ].join('\n'); - const result = ensureOnsidePodIOS(podfileWithPostInstall); - expect(result).toContain("pod 'OnsideKit'"); - expect(result).toContain('# [expo-iap] Make OnsideKit visible'); - // Should not create a second post_install block - const postInstallCount = ( - result.match(/post_install do \|installer\|/g) ?? [] - ).length; - expect(postInstallCount).toBe(1); - }); - - it('skips if OnsideKit and post_install hook already exist', () => { - const podfileComplete = [ - "target 'MyApp' do", - " pod 'OnsideKit', :podspec => 'https://example.com'", - 'end', - '', - 'post_install do |installer|', - ' # [expo-iap] Make OnsideKit visible', - 'end', - ].join('\n'); - const result = ensureOnsidePodIOS(podfileComplete); - expect(result).toBe(podfileComplete); - }); - - it('adds post_install hook when OnsideKit already exists', () => { - const podfileWithOnsideKit = [ - "target 'MyApp' do", - " pod 'OnsideKit', :podspec => 'https://example.com'", + " pod 'ExpoIap/Onside', :path => '../node_modules/expo-iap/ios'", 'end', ].join('\n'); - const result = ensureOnsidePodIOS(podfileWithOnsideKit); - expect(result).toContain('# [expo-iap] Make OnsideKit visible'); - // Should not add a duplicate OnsideKit pod - const onsideKitCount = (result.match(/pod 'OnsideKit'/g) ?? []).length; - expect(onsideKitCount).toBe(1); + const result = ensureOnsidePodIOS(podfileWithSubspec); + expect(result).toBe(podfileWithSubspec); }); it('returns unchanged content when no target block found', () => { @@ -275,8 +234,7 @@ describe('ensureOnsidePodIOS', () => { } expect(content).toBe(basePodfile); - expect(content).not.toContain("pod 'OnsideKit'"); - expect(content).not.toContain('# [expo-iap] Make OnsideKit visible'); + expect(content).not.toContain("pod 'ExpoIap/Onside'"); }); it('modifies Podfile when onside is enabled', () => { @@ -288,7 +246,6 @@ describe('ensureOnsidePodIOS', () => { } expect(content).not.toBe(basePodfile); - expect(content).toContain("pod 'OnsideKit'"); - expect(content).toContain('# [expo-iap] Make OnsideKit visible'); + expect(content).toContain("pod 'ExpoIap/Onside'"); }); }); diff --git a/plugin/src/withIAP.ts b/plugin/src/withIAP.ts index e5a8c1342..716d280e0 100644 --- a/plugin/src/withIAP.ts +++ b/plugin/src/withIAP.ts @@ -262,75 +262,28 @@ const withIapAndroid: ConfigPlugin< return config; }; -const ONSIDEKIT_PODSPEC_URL = - 'https://raw.githubusercontent.com/onside-io/OnsideKit-iOS/0.5.0/OnsideKit.podspec'; - -// post_install hook that adds OnsideKit's module to ExpoIap's search paths -// so that `#if canImport(OnsideKit)` resolves correctly at build time. -// This is needed because OnsideKit is not yet on CocoaPods CDN, so we cannot -// use a podspec `s.dependency`. Once OnsideKit is on CDN, replace with a subspec. -const ONSIDE_POST_INSTALL_HOOK = ` - # [expo-iap] Make OnsideKit visible to ExpoIap for #if canImport(OnsideKit) - installer.pods_project.targets.each do |target| - if target.name == 'ExpoIap' - target.build_configurations.each do |config| - swift_paths = config.build_settings['SWIFT_INCLUDE_PATHS'] || '$(inherited)' - onside_path = '\${PODS_CONFIGURATION_BUILD_DIR}/OnsideKit' - unless swift_paths.include?(onside_path) - config.build_settings['SWIFT_INCLUDE_PATHS'] = "#{swift_paths} #{onside_path}" - end - end - end - end`; - -const ONSIDE_POST_INSTALL_MARKER = '# [expo-iap] Make OnsideKit visible'; +const EXPO_IAP_IOS_PATH = '../node_modules/expo-iap/ios'; export const ensureOnsidePodIOS = (content: string): string => { - const alreadyHasOnsideKit = /^\s*pod\s+['"]OnsideKit['"].*$/m.test(content); - const alreadyHasPostInstall = content.includes(ONSIDE_POST_INSTALL_MARKER); - - if (alreadyHasOnsideKit && alreadyHasPostInstall) { + if (/^\s*pod\s+['"]ExpoIap\/Onside['"].*$/m.test(content)) { return content; } - let result = content; - - // 1) Add OnsideKit pod inside the target block - if (!alreadyHasOnsideKit) { - const targetMatch = result.match(/target\s+'[^']+'\s+do\s*\n/); - if (!targetMatch) { - WarningAggregator.addWarningIOS( - 'expo-iap', - 'Could not find a target block in Podfile when adding OnsideKit; skipping installation.', - ); - return content; - } - - const podLine = ` pod 'OnsideKit', :podspec => '${ONSIDEKIT_PODSPEC_URL}'\n`; - const insertIndex = targetMatch.index! + targetMatch[0].length; - result = result.slice(0, insertIndex) + podLine + result.slice(insertIndex); + const targetMatch = content.match(/target\s+'[^']+'\s+do\s*\n/); + if (!targetMatch) { + WarningAggregator.addWarningIOS( + 'expo-iap', + 'Could not find a target block in Podfile when adding ExpoIap/Onside; skipping installation.', + ); + return content; } - // 2) Add post_install hook to make OnsideKit visible to ExpoIap - if (!alreadyHasPostInstall) { - const postInstallMatch = result.match(/post_install\s+do\s+\|installer\|/); - if (postInstallMatch) { - // Append inside existing post_install block - const insertIndex = postInstallMatch.index! + postInstallMatch[0].length; - result = - result.slice(0, insertIndex) + - '\n' + - ONSIDE_POST_INSTALL_HOOK + - result.slice(insertIndex); - } else { - // Create new post_install block - result += `\npost_install do |installer|${ONSIDE_POST_INSTALL_HOOK}\nend\n`; - } - } + const podLine = ` pod 'ExpoIap/Onside', :path => '${EXPO_IAP_IOS_PATH}'\n`; + const insertIndex = targetMatch.index! + targetMatch[0].length; - logOnce('๐Ÿ“ฆ expo-iap: Added OnsideKit pod and post_install hook to Podfile'); + logOnce('๐Ÿ“ฆ expo-iap: Added ExpoIap/Onside subspec to Podfile'); - return result; + return content.slice(0, insertIndex) + podLine + content.slice(insertIndex); }; export type AutolinkState = {expoIap: boolean; onside: boolean};