Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion ios/ExpoIap.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ 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
# 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 = {
'DEFINES_MODULE' => 'YES',
Expand Down
68 changes: 67 additions & 1 deletion plugin/__tests__/withIAP.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {ExpoConfig} from '@expo/config-types';
import {
computeAutolinkModules,
ensureOnsidePodIOS,
modifyAppBuildGradle,
resolveModuleSelection,
} from '../src/withIAP';
Expand Down Expand Up @@ -28,7 +29,7 @@ jest.mock('expo/config-plugins', () => {

return {
...plugins,
WarningAggregator: {addWarningAndroid: jest.fn()},
WarningAggregator: {addWarningAndroid: jest.fn(), addWarningIOS: jest.fn()},
};
});

Expand Down Expand Up @@ -183,3 +184,68 @@ describe('ios module selection', () => {
});
});
});

describe('ensureOnsidePodIOS', () => {
const basePodfile = [
"source 'https://cdn.cocoapods.org/'",
'',
"target 'MyApp' do",
" pod 'ExpoModulesCore'",
'end',
'',
].join('\n');

it('adds ExpoIap/Onside subspec pod', () => {
const result = ensureOnsidePodIOS(basePodfile);
expect(result).toContain("pod 'ExpoIap/Onside'");
});

it('inserts pod inside the target block', () => {
const result = ensureOnsidePodIOS(basePodfile);
const targetIndex = result.indexOf("target 'MyApp' do");
const subspecIndex = result.indexOf("pod 'ExpoIap/Onside'");
const endIndex = result.indexOf('end');
expect(subspecIndex).toBeGreaterThan(targetIndex);
expect(subspecIndex).toBeLessThan(endIndex);
});

it('skips if 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).toBe(podfileWithSubspec);
});

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 '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 'ExpoIap/Onside'");
});
});
20 changes: 8 additions & 12 deletions plugin/src/withIAP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -263,31 +262,28 @@ 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 EXPO_IAP_IOS_PATH = '../node_modules/expo-iap/ios';

if (podRegex.test(content)) {
export const ensureOnsidePodIOS = (content: string): string => {
if (/^\s*pod\s+['"]ExpoIap\/Onside['"].*$/m.test(content)) {
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 OnsideKit; skipping installation.',
'Could not find a target block in Podfile when adding ExpoIap/Onside; skipping installation.',
);
return content;
}

const podLine = ` 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);

logOnce('📦 expo-iap: Added OnsideKit pod to Podfile');
logOnce('📦 expo-iap: Added ExpoIap/Onside subspec to Podfile');

return `${before}${podLine}\n${after}`;
return content.slice(0, insertIndex) + podLine + content.slice(insertIndex);
};
Comment on lines +267 to 287
Copy link

@coderabbitai coderabbitai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Checking CocoaPods availability for OnsideKit..."
status=$(curl -s -o /tmp/onsidekit.html -w "%{http_code}" https://cocoapods.org/pods/OnsideKit || true)
echo "HTTP status (https://cocoapods.org/pods/OnsideKit): $status"

echo
echo "Checking current plugin pod injection logic..."
rg -n "ensureOnsidePodIOS|ExpoIap/Onside|OnsideKit" plugin/src/withIAP.ts

echo
echo "Expected verification outcome:"
echo "- If status is not 200 and plugin does not inject a direct OnsideKit podspec fallback,"
echo "  Onside-enabled Pod install can fail on dependency resolution."

Repository: hyochan/expo-iap

Length of output: 838


🏁 Script executed:

cat -n ios/ExpoIap.podspec

Repository: hyochan/expo-iap

Length of output: 2087


🏁 Script executed:

rg -A 10 "subspec.*[Oo]nside" ios/ExpoIap.podspec

Repository: hyochan/expo-iap

Length of output: 297


🏁 Script executed:

find . -name "*.podspec" -o -name "*.md" | xargs rg -l "OnsideKit|Onside" 2>/dev/null | head -20

Repository: hyochan/expo-iap

Length of output: 362


🏁 Script executed:

head -100 docs/docs/guides/onside-integration.md

Repository: hyochan/expo-iap

Length of output: 3759


🏁 Script executed:

head -100 docs/blog/2026-03-15-3.4.10-onside-integration.md

Repository: hyochan/expo-iap

Length of output: 3131


🏁 Script executed:

rg -n "OnsideKit|podspec|source|private" plugin/src/withIAP.ts | head -30

Repository: hyochan/expo-iap

Length of output: 330


🏁 Script executed:

rg -B 5 -A 5 "ensureOnsidePodIOS" plugin/src/withIAP.ts | head -50

Repository: hyochan/expo-iap

Length of output: 653


🏁 Script executed:

rg -n "OnsideKit" README.md docs/

Repository: hyochan/expo-iap

Length of output: 42


🏁 Script executed:

grep -r "OnsideKit" . --include="*.json" --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v node_modules

Repository: hyochan/expo-iap

Length of output: 145


🏁 Script executed:

grep -r "OnsideKit\|onside-kit\|onside_kit" . --include="*.md" --include="*.json" 2>/dev/null | grep -v node_modules | grep -v ".next"

Repository: hyochan/expo-iap

Length of output: 42


🏁 Script executed:

cat plugin/src/withIAP.ts | sed -n '475,510p'

Repository: hyochan/expo-iap

Length of output: 1255


ExpoIap/Onside injection will break pod install for Onside-enabled users due to unresolvable OnsideKit dependency.

The Onside subspec (ios/ExpoIap.podspec:32) declares ss.dependency 'OnsideKit', but OnsideKit is not available on the public CocoaPods CDN (HTTP 404). When users enable Onside via the plugin, pod install fails during dependency resolution. The plugin must either inject a CocoaPods source for OnsideKit or implement an alternative dependency resolution strategy. Currently, documentation presents Onside as ready to use with "Easy Setup," but enabling it produces a broken build.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/src/withIAP.ts` around lines 267 - 287, ensureOnsidePodIOS currently
injects "pod 'ExpoIap/Onside'" unconditionally which breaks pod install because
the OnsideKit dependency isn't on the public CocoaPods CDN; update
ensureOnsidePodIOS to either (A) inject the required CocoaPods source/repo entry
before inserting the podLine (so the resolver can find OnsideKit) or (B) avoid
adding the subspec and instead emit a clear WarningAggregator.addWarningIOS
message instructing the user how to add the private source or enable Onside
manually; locate the logic in ensureOnsidePodIOS (look for EXPO_IAP_IOS_PATH,
podLine, targetMatch) and implement one of these fixes so pod install can
resolve OnsideKit.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — this is a known blocker. OnsideKit is not on CocoaPods CDN yet (confirmed by the OnsideKit team). We've asked them to publish it. Once it's on CDN, this subspec approach works as-is with no code changes needed. This PR is intentionally written for that scenario.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hyochan, understood — thanks for the context. Since this approach is intentionally forward-looking for when OnsideKit lands on the CDN, that makes sense.

One optional suggestion: it may be worth adding a temporary WarningAggregator.addWarningIOS note inside ensureOnsidePodIOS (or in the docs) to surface a clear message to users who enable Onside before OnsideKit is published — so they get an actionable error rather than a cryptic pod install failure. This can be removed once CDN availability is confirmed.

That said, if you prefer to keep it minimal until the CDN publish is done, that's reasonable too.


✏️ Learnings added
Learnt from: hyochan
Repo: hyochan/expo-iap PR: 321
File: plugin/src/withIAP.ts:267-287
Timestamp: 2026-03-17T16:08:04.425Z
Learning: In `expo-iap`, the `ExpoIap/Onside` subspec in `ios/ExpoIap.podspec` declares `ss.dependency 'OnsideKit'`, but OnsideKit is NOT yet published to the public CocoaPods CDN (as of March 2026). The PR using this subspec approach is intentionally written for when OnsideKit becomes available on CDN — at that point no code changes are needed. The OnsideKit team has been contacted to publish it.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: hyochan/expo-iap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-16T23:18:39.882Z
Learning: Applies to ios/ExpoIap.podspec : Never modify the iOS platform version in `ios/ExpoIap.podspec` - it MUST remain at `13.4` due to Expo SDK compatibility, even though code requires iOS 15.0+

Learnt from: CR
Repo: hyochan/expo-iap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-16T23:18:39.882Z
Learning: Applies to ios/ExpoIap.podspec : Ensure tvOS platform version in `ios/ExpoIap.podspec` is set to `16.0` (not `13.4`) because the `openiap` CocoaPods dependency requires tvOS 16.0 minimum

Learnt from: CR
Repo: hyochan/expo-iap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-16T23:18:39.882Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Functions that only operate on one platform must carry the suffix: `nameIOS` or `nameAndroid` (e.g., `getStorefrontIOS`, `deepLinkToSubscriptionsAndroid`)

Learnt from: CR
Repo: hyochan/expo-iap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-16T23:18:39.882Z
Learning: Applies to **/*.{ts,tsx} : Inside the `useIAP` hook, most methods return `Promise<void>` and update internal state. Do not design examples or implementations that expect data from methods like `fetchProducts`, `requestPurchase`, `getAvailablePurchases`, `getActiveSubscriptions`. Consume state from hook instead

Learnt from: hyochan
Repo: hyochan/expo-iap PR: 198
File: android/src/main/java/expo/modules/iap/ExpoIapModule.kt:39-49
Timestamp: 2025-09-12T03:46:34.343Z
Learning: In expo-iap codebase, maintainer hyochan prefers simpler implementations over complex safety measures when the additional complexity isn't justified by the specific use case requirements.


export type AutolinkState = {expoIap: boolean; onside: boolean};
Expand Down Expand Up @@ -493,7 +489,7 @@ const withIapIOS: ConfigPlugin<WithIapIosOptions | undefined> = (

// 3) Optionally install OnsideKit when enabled in config
if (options?.enableOnside) {
content = ensureOnsidePod(content);
content = ensureOnsidePodIOS(content);
}

config.modResults.contents = content;
Expand Down
Loading