Skip to content

feat: support local IAPKit receipt verification - #225

Merged
hyochan merged 17 commits into
mainfrom
feat/iapkit-local-receipt-verification
Jul 13, 2026
Merged

hyochan merged 17 commits into
mainfrom
feat/iapkit-local-receipt-verification

Conversation

@hyochan

@hyochan hyochan commented Jul 13, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Add optional iapkit.baseUrl support across OpenIAP native packages and framework bridges, with HTTP(S)-origin validation and same-deployment API-key guidance.
  • Add Local (IAPKit) to the React Native and Expo Martie examples in the exact order Local (Device) → Local (IAPKit) → IAPKit → None (Skip).
  • Harden the compiled IAPKit server smoke test, add a dedicated Martie local-receipt E2E workflow/skill, and make Godot Apple framework builds/load checks fail closed.
  • Bump the OpenIAP spec patch from 2.3.0 to 2.3.1 and add the API availability note to generated SDK types and public docs.

Root cause

IAPKit receipt verification was fixed to the hosted endpoint in native and Vega paths, so a device purchase could not be routed through the locally compiled IAPKit server. The examples also had no distinct local-server choice, and the server smoke probe could accidentally succeed against an older process already owning the port.

Validation

Live receipt vertical

  • Physical iPhone 13 mini, Expo example dev.hyo.martie, Apple sandbox.
  • SKU: dev.hyo.martie.10bulbs.
  • Compiled local packages/kit server backed by the real Martie Dev Convex deployment.
  • Correlation ID: 891e06ad-01d0-419e-8f2a-c77b156ae037.
  • Local server returned HTTP 200 with isValid: true / ready-to-consume; the app finished the transaction.
  • Matching Dev Convex purchase row: k9751qztk3dp92y9pq3vks3dcx8ae84s.
  • No additional purchase was made during the final review.

Device UI preview

  • Built and installed the current React Native example on a physical Pixel 2.
  • Confirmed all four verification choices appear in the requested order and selecting Local (IAPKit) updates the purchase screen.
  • A short screen recording is attached in the PR conversation.

Automated and build checks

  • GQL generation and tests: 47 passed.
  • IAPKit: lint, Prettier, 51 test files / 572 tests, production build, compiled-server smoke including POST /v1/purchase/verify → 400.
  • React Native and Expo focused/full adapter and example tests, including Kepler/Vega URL edge cases.
  • Apple URL tests, Google validator tests, Flutter tests/analyze, KMP Android/iOS compiles, MAUI net9/net10 builds.
  • Godot Android bridge test; iOS/macOS paired frameworks rebuilt; native ClassDB load plus 103 types and 73 wrapper tests passed.
  • bun audit:parity, bun audit:docs, bun audit:release-state, docs typecheck/format, and git diff --check.

Release gate

After merge, release the Apple package and required Google flavors first. Then sync the new native version pins and rebuild downstream artifacts before releasing React Native, Expo, Flutter, KMP, or Godot. Monorepo tests use local-project substitution; publishing downstream SDKs against the current Apple/Google pins would otherwise discard baseUrl.

Summary by CodeRabbit

  • New Features
    • Added Local (IAPKit) purchase verification with a configurable IAPKit server base URL.
    • Added a verification method selector/modal, including Local Device, Local (IAPKit), hosted IAPKit, and Skip.
    • Extended the configurable base URL through React Native, Expo, Flutter, Kotlin, Swift, MAUI, and Godot integrations.
  • Documentation
    • Expanded setup and end-to-end receipt verification guidance for hosted vs local flows, including device routing and validation steps.
  • Bug Fixes
    • Improved custom IAPKit endpoint handling with stricter HTTP(S) origin validation and safer request URL construction.
  • Tests
    • Added/expanded unit, adapter, and E2E coverage for local receipt verification and base URL forwarding.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@hyochan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dcf20e9f-b5f3-4208-b1e1-1910d1dae849

📥 Commits

Reviewing files that changed from the base of the PR and between 38b065a and 42da3d8.

📒 Files selected for processing (8)
  • .codex/skills/iapkit-e2e-martie/SKILL.md
  • .github/workflows/release-google.yml
  • libraries/expo-iap/src/__tests__/vega-adapter.test.ts
  • libraries/expo-iap/src/vega-adapter.ts
  • libraries/react-native-iap/src/__tests__/vega-adapter.test.ts
  • libraries/react-native-iap/src/vega-adapter.ts
  • scripts/audit-non-godot-parity.mjs
  • scripts/sync-versions.sh
📝 Walkthrough

Walkthrough

This PR adds configurable IAPKit verification endpoints across OpenIAP contracts, adapters, native bridges, and examples. It introduces Local (IAPKit) purchase flows, stricter URL validation, receipt E2E runbooks, smoke-server probes, and Godot framework packaging changes.

Changes

IAPKit verification

Layer / File(s) Summary
Verification contracts and endpoint validation
libraries/*/src/types.*, packages/*, openiap-versions.json
Adds optional baseUrl support, specification updates, endpoint normalization, and strict HTTP(S) origin validation across JavaScript, Swift, Kotlin, and GraphQL implementations.
Native bridge propagation
libraries/flutter_inapp_purchase/..., libraries/kmp-iap/..., libraries/react-native-iap/..., packages/apple/..., libraries/godot-iap/...
Forwards baseUrl through platform bridges, JSON models, Objective-C selectors, and Godot provider normalization with bridge tests.
Local verification example flow
libraries/react-native-iap/example/..., libraries/expo-iap/example/...
Adds Local (IAPKit) selection, device-reachable configuration, resolved base URLs, provider verification, modal/action-sheet controls, and purchase/subscription coverage.
Receipt E2E and smoke validation
.claude/commands/e2e-tests.md, .codex/skills/iapkit-e2e-martie/*, packages/kit/scripts/smoke-server.sh
Documents smoke and live receipt lanes, local server routing, device setup, approval gates, evidence requirements, cleanup, and malformed verification probes.

Godot packaging

Layer / File(s) Summary
Framework packaging and native checks
libraries/godot-iap/Makefile, libraries/godot-iap/scripts/*, libraries/godot-iap/addons/*, libraries/godot-iap/Example/tests/*
Adds shared Apple framework installation, macOS signing/link handling, native-extension tests, updated Android artifacts, and the OpenIAP Google dependency update.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • hyodotdev/openiap#46: Introduced provider-based IAPKit verification surfaces used by this local verification flow.
  • hyodotdev/openiap#53: Added related verification request structures that this PR extends with baseUrl.
  • hyodotdev/openiap#102: Covers the adjacent IAPKit verification host and endpoint construction codepath.

Suggested labels: ❄️ types, :rabbit2: server, :tea: integration

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding local IAPKit receipt verification support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/iapkit-local-receipt-verification

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hyochan hyochan added cross-platform Cross-platform (both Android & iOS) expo-iap expo-iap library flutter_inapp_purchase flutter_inapp_purchase library godot-iap godot-iap library kit IAPKit (receipt-validation SaaS) kmp-iap kmp-iap library maui react-native-iap react-native-iap library ⬡ protocol 🎯 feature New feature 👀 example 📖 documentation Improvements or additions to documentation 📱 iOS Related to iOS 🤖 android Related to android 🧪 test Issue or pr related to testing labels Jul 13, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request updates the OpenIAP specification to version 2.3.1, introducing support for a custom baseUrl in IAPKit purchase verification to enable self-hosted or local testing. The changes span multiple client libraries (Expo, React Native, Flutter, KMP, Maui, Godot) and native packages (Android, iOS), adding the baseUrl property to verification parameters, updating native bridges, implementing robust HTTP(S) origin validation, and adding a 'Local (IAPKit)' option to example apps. E2E testing documentation, skills, and local server smoke tests have also been updated to support this workflow. As there are no review comments provided, I have no feedback to evaluate.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@hyochan

hyochan commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Physical Pixel 2 preview (no purchase): verification modes appear in the required order — Local (Device), Local (IAPKit), IAPKit, None (Skip) — and selecting Local (IAPKit) is reflected in the UI.

openiap-iapkit-selector.mp4

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements support for a custom baseUrl in the IAPKit purchase verification provider across all platform packages and framework libraries (React Native, Expo, Flutter, Godot, KMP, MAUI, Apple, and Google), upgrading the OpenIAP Spec to version 2.3.1. This allows developers to route verification requests to self-hosted or local IAPKit servers. The updates include GraphQL schema modifications, native bridge forwarding, strict HTTP(S) origin validation, and comprehensive enhancements to the example apps (such as a new "Local (IAPKit)" option and selector modal) along with E2E test documentation and skills. There are no review comments to address, and I have no additional feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the OpenIAP purchase-verification surface to support self-hosted / locally compiled IAPKit receipt verification by introducing an optional baseUrl (validated as an HTTP(S) origin) and wiring it end-to-end across native packages, framework bridges, examples, docs, and E2E guidance.

Changes:

  • Add optional baseUrl to the iapkit verification props (spec 2.3.1) and propagate it through generated SDK types, Apple/Google native implementations, and multiple framework bridges.
  • Update React Native + Expo Martie examples to include Local (IAPKit) as a distinct verification choice (in the required order) and ensure hosted IAPKit can intentionally omit a configured local URL.
  • Harden packages/kit compiled-server smoke checks and add/extend skills + E2E documentation for a Martie local-receipt vertical; tighten Godot Apple framework installation and fail-closed load checks.

Reviewed changes

Copilot reviewed 69 out of 82 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/kit/scripts/smoke-server.sh Harden compiled-server smoke by confirming port ownership via log marker and probing malformed POST /v1/purchase/verify for a 400.
packages/kit/README.md Update smoke-server documentation to match the hardened probes and behavior.
packages/kit/CONVENTION.md Update conventions doc for the new smoke-server readiness check and verify probe.
packages/gql/src/type.graphql Add baseUrl to the GraphQL input schema with spec availability and deployment guidance.
packages/gql/src/generated/types.ts Generated TS types include baseUrl on IAPKit verification props.
packages/gql/src/generated/Types.swift Generated Swift types include baseUrl on IAPKit verification props.
packages/gql/src/generated/Types.kt Generated Kotlin types include baseUrl on IAPKit verification props (JSON in/out).
packages/gql/src/generated/types.gd Generated GDScript types include base_url mapping to/from baseUrl.
packages/gql/src/generated/types.dart Generated Dart types include baseUrl (JSON in/out).
packages/gql/src/generated/Types.cs Generated C# types include BaseUrl for IAPKit verification props.
packages/gql/package.json Bump GQL package version to 2.3.1.
packages/google/openiap/src/test/java/dev/hyo/openiap/PurchaseVerificationValidatorTest.kt Add/extend Android native tests for defaulting, normalization, and rejection of malformed baseUrl.
packages/google/openiap/src/main/java/dev/hyo/openiap/utils/PurchaseVerificationValidator.kt Implement Android-side IAPKit endpoint resolution + strict origin validation before verifying.
packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt Synced Google package Kotlin types include baseUrl.
packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx Document the new baseUrl field and its constraints in the docs site.
packages/docs/package.json Bump docs package version to 2.3.1.
packages/docs/openiap-versions.json Update docs’ spec pin to 2.3.1.
packages/apple/Tests/OpenIapTests/VerifyPurchaseWithProviderTests.swift Add iOS/macOS tests for default URL behavior, trimming, malformed override rejection, and ObjC selector parity.
packages/apple/Sources/OpenIapModule+ObjC.swift Add an ObjC bridge selector that forwards baseUrl while preserving the legacy selector.
packages/apple/Sources/OpenIapModule.swift Centralize and validate IAPKit verification URL construction via iapkitVerificationURL(baseUrl:).
packages/apple/Sources/Models/Types.swift Synced Apple package Swift types include baseUrl.
openiap-versions.json Bump monorepo spec pin to 2.3.1.
libraries/react-native-iap/src/vega-adapter.ts Add robust origin-only URL parsing/validation for Kepler/Vega and use it for IAPKit verify requests.
libraries/react-native-iap/src/types.ts Synced RN types include baseUrl.
libraries/react-native-iap/src/specs/RnIap.nitro.ts Extend Nitro contract to include baseUrl for iapkit props.
libraries/react-native-iap/src/tests/vega-adapter.test.ts Add parameterized tests for valid/invalid baseUrl, including IPv6 and Kepler URL limitations.
libraries/react-native-iap/src/tests/index.test.ts Extend public API tests to cover baseUrl in verify payload shape.
libraries/react-native-iap/src/tests/iapkit-base-url-bridge.test.js Ensure bridge parity by asserting baseUrl appears in the Nitro spec and native forwarding maps.
libraries/react-native-iap/ios/HybridRnIap.swift Forward baseUrl through the iOS native payload mapping.
libraries/react-native-iap/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt Forward baseUrl through the Android native payload mapping.
libraries/react-native-iap/example/src/utils/vegaRuntime.ts Add helpers to require API key, resolve local-vs-hosted base URL, and ensure payloads omit local URL when hosted is selected.
libraries/react-native-iap/example/src/hooks/useVerificationMethod.ts Expand verification method state to include iapkit-localhost and support Android modal selection.
libraries/react-native-iap/example/src/components/VerificationMethodSelectorModal.tsx New Android-friendly modal UI that lists verification options in the required order.
libraries/react-native-iap/example/screens/SubscriptionFlow.tsx Integrate Local (IAPKit) choice, route baseUrl appropriately, and wire selector modal.
libraries/react-native-iap/example/screens/PurchaseFlow.tsx Integrate Local (IAPKit) choice, route baseUrl appropriately, and wire selector modal.
libraries/react-native-iap/example/README.md Add concrete setup instructions for hosted vs local IAPKit verification and adb reverse guidance.
libraries/react-native-iap/example/.env.example Clarify hosted vs local key requirements and base URL expectations.
libraries/react-native-iap/example/tests/utils/vegaRuntime.test.ts Add tests for default method selection and stricter API key/baseUrl requirements.
libraries/react-native-iap/example/tests/screens/SubscriptionFlow.test.tsx Add tests for correct default selection, method switching, and baseUrl forwarding/omission.
libraries/react-native-iap/example/tests/screens/PurchaseFlow.test.tsx Add tests for correct ordering, routing via local server, and hosted omission of baseUrl.
libraries/maui-iap/src/OpenIap.Maui/Types.cs Synced MAUI types include BaseUrl.
libraries/kmp-iap/library/src/iosMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseIOS.kt Forward baseUrl via the iOS ObjC bridge call.
libraries/kmp-iap/library/src/commonTest/kotlin/io/github/hyochan/kmpiap/VerificationTest.kt Extend KMP tests to ensure baseUrl is serialized.
libraries/kmp-iap/library/src/commonMain/kotlin/io/github/hyochan/kmpiap/openiap/Types.kt Synced KMP OpenIAP types include baseUrl (JSON in/out).
libraries/kmp-iap/library/src/androidUnitTest/kotlin/io/github/hyochan/kmpiap/IapkitBaseUrlBridgeTest.kt Add a bridge regression test asserting Android forwards baseUrl.
libraries/kmp-iap/library/src/androidMain/kotlin/io/github/hyochan/kmpiap/InAppPurchaseAndroid.kt Wire baseUrl into the Android-side IAPKit props payload.
libraries/godot-iap/scripts/install_apple_framework.sh New helper to normalize Apple frameworks, preserve plist when possible, and enforce fail-closed macOS signing/rpath setup.
libraries/godot-iap/Makefile Use the installer script for iOS/macOS frameworks and add a native extension load test to make test.
libraries/godot-iap/Example/tests/test_native_extension.gd New macOS-only headless test to ensure the tracked native extension can load/instantiate.
libraries/godot-iap/android/src/test/java/dev/hyo/godotiap/GodotIapVerificationBridgeTest.kt Add tests to ensure legacy and nested iapkit payloads include/pass baseUrl.
libraries/godot-iap/android/src/main/java/dev/hyo/godotiap/GodotIap.kt Extract/extend provider-props normalization to include baseUrl for legacy maps.
libraries/godot-iap/addons/godot-iap/types.gd Synced Godot addon types include base_url mapping to/from baseUrl.
libraries/godot-iap/addons/godot-iap/android/GodotIap.gdap Update remote dependency pin for openiap-google to 2.3.1.
libraries/flutter_inapp_purchase/test/iapkit_base_url_bridge_test.dart Add test asserting iOS/macos/Android native plugins forward baseUrl.
libraries/flutter_inapp_purchase/test/flutter_inapp_purchase_channel_test.dart Add test asserting Dart API forwards baseUrl into the native payload.
libraries/flutter_inapp_purchase/macos/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift Forward baseUrl in the macOS plugin iapkit map.
libraries/flutter_inapp_purchase/lib/types.dart Synced Flutter types include baseUrl (JSON in/out).
libraries/flutter_inapp_purchase/lib/flutter_inapp_purchase.dart Include baseUrl in the verifyPurchaseWithProvider method channel payload when set.
libraries/flutter_inapp_purchase/ios/flutter_inapp_purchase/Sources/flutter_inapp_purchase/FlutterInappPurchasePlugin.swift Forward baseUrl in the iOS plugin iapkit map.
libraries/flutter_inapp_purchase/android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt Forward baseUrl in the Android plugin iapkit map.
libraries/expo-iap/src/vega-adapter.ts Add origin-only base URL parsing/validation (Kepler-safe) and use it for IAPKit fetch requests.
libraries/expo-iap/src/types.ts Synced Expo types include baseUrl.
libraries/expo-iap/src/tests/vega-adapter.test.ts Add parameterized tests for valid/invalid base URLs, including IPv6 and Kepler URL limitations.
libraries/expo-iap/example/src/utils/vegaRuntime.ts Add local-vs-hosted base URL resolver, method defaults, and stricter key requirements.
libraries/expo-iap/example/README.md Add hosted vs local verification setup instructions and adb reverse guidance.
libraries/expo-iap/example/app/subscription-flow.tsx Add Local (IAPKit) selection, route local baseUrl only for local mode, and update labels/messages.
libraries/expo-iap/example/app/purchase-flow.tsx Add Local (IAPKit) selection, route local baseUrl only for local mode, and update labels/messages.
libraries/expo-iap/example/.env.example Clarify hosted vs local key requirements and base URL expectations.
libraries/expo-iap/example/tests/vega-runtime.test.ts Extend tests for default method selection and baseUrl omission/requirements.
libraries/expo-iap/example/tests/subscription-flow.test.tsx Add tests for required verification option ordering and baseUrl forwarding behavior.
libraries/expo-iap/example/tests/purchase-flow.test.tsx Add tests for required ordering and correct routing of local vs hosted IAPKit requests.
bun.lock Sync lockfile metadata for the 2.3.1 package version bumps.
.codex/skills/review-self/SKILL.md Reference the new Martie local-receipt E2E skill.
.codex/skills/iapkit-e2e-martie/SKILL.md New detailed skill/runbook for the Martie Local (IAPKit) live receipt vertical, including safety gates.
.codex/skills/iapkit-e2e-martie/agents/openai.yaml Agent metadata wiring for the new Martie E2E skill.
.claude/commands/e2e-tests.md Add the local IAPKit Martie receipt vertical to the E2E matrix and document prerequisites/reporting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@hyochan
hyochan marked this pull request as ready for review July 13, 2026 17:28
@hyochan
hyochan requested a review from Copilot July 13, 2026 17:29

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for local and self-hosted IAPKit receipt verification across multiple framework libraries (including React Native, Expo, Flutter, Godot, KMP, and MAUI) by adding a 'baseUrl' property to the OpenIAP Spec (v2.3.1). The feedback points out several issues: the use of non-POSIX '[[' syntax in 'smoke-server.sh' which can cause syntax errors, a potential SSRF vulnerability in the IPv4 validation regex of the Vega adapters due to allowed leading zeros, and TypeScript type-widening issues in the example runtime helpers.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

sleep 0.25
done

if [[ "$ready" -ne 1 ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Using [[ is a Bash/Zsh extension and is not POSIX-compliant. If this script is executed with /bin/sh (which defaults to dash on Debian/Ubuntu systems), it will result in a syntax error. Use the standard [ instead.

Suggested change
if [[ "$ready" -ne 1 ]]; then
if [ "$ready" -ne 1 ]; then

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this: smoke-server.sh is explicitly a Bash executable (#!/usr/bin/env bash), and every repository invocation executes ./scripts/smoke-server.sh directly. It already relies on Bash-only BASH_SOURCE, pipefail, local, and multiple existing [[ expressions, so replacing this one condition would neither improve nor promise POSIX sh compatibility. bash -n passes.

-H "Content-Type: application/json" \
--data '{}' \
"http://localhost:${PORT}${path}")"
if [[ "$code" != "$expected" ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Using [[ is a Bash/Zsh extension and is not POSIX-compliant. Use the standard [ instead to ensure compatibility with all POSIX-compliant shells.

Suggested change
if [[ "$code" != "$expected" ]]; then
if [ "$code" != "$expected" ]; then

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this: smoke-server.sh is explicitly a Bash executable (#!/usr/bin/env bash), and every repository invocation executes ./scripts/smoke-server.sh directly. The script already uses Bash-only BASH_SOURCE, pipefail, local, and existing [[ expressions, so changing only this comparison would not make it POSIX sh compatible. bash -n passes.

Comment thread libraries/expo-iap/src/vega-adapter.ts
Comment thread libraries/react-native-iap/src/vega-adapter.ts
Comment on lines +58 to +74
export function resolveIapkitVerificationBaseUrl(
method: 'iapkit-localhost' | 'iapkit',
configuredBaseUrl: string | null | undefined = getConfiguredIapkitBaseUrl(),
): string | undefined {
if (method === 'iapkit') {
return undefined;
}

const baseUrl = configuredBaseUrl?.trim();
if (!baseUrl) {
throw new Error(
'EXPO_PUBLIC_IAPKIT_BASE_URL not configured for Local (IAPKit) verification',
);
}

return baseUrl;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The method parameter is typed as 'iapkit-localhost' | 'iapkit', but the callers in purchase-flow.tsx and subscription-flow.tsx pass currentVerificationMethod which is of the wider VerificationMethod type. This can cause TypeScript compilation errors due to type widening. Typing the parameter as VerificationMethod and checking method !== 'iapkit-localhost' avoids this issue.

Suggested change
export function resolveIapkitVerificationBaseUrl(
method: 'iapkit-localhost' | 'iapkit',
configuredBaseUrl: string | null | undefined = getConfiguredIapkitBaseUrl(),
): string | undefined {
if (method === 'iapkit') {
return undefined;
}
const baseUrl = configuredBaseUrl?.trim();
if (!baseUrl) {
throw new Error(
'EXPO_PUBLIC_IAPKIT_BASE_URL not configured for Local (IAPKit) verification',
);
}
return baseUrl;
}
export function resolveIapkitVerificationBaseUrl(
method: VerificationMethod,
configuredBaseUrl: string | null | undefined = getConfiguredIapkitBaseUrl(),
): string | undefined {
if (method !== 'iapkit-localhost') {
return undefined;
}
const baseUrl = configuredBaseUrl?.trim();
if (!baseUrl) {
throw new Error(
'EXPO_PUBLIC_IAPKIT_BASE_URL not configured for Local (IAPKit) verification',
);
}
return baseUrl;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this: the call occurs only in the else branch after ignore and local have both been excluded, so TypeScript narrows currentVerificationMethod to iapkit-localhost | iapkit. The strict Expo example typecheck passes. Widening the helper to VerificationMethod would hide invalid ignore/local calls and weaken its IAPKit-only contract.

Comment on lines +22 to +38
export function resolveIapkitVerificationBaseUrl(
method: 'iapkit-localhost' | 'iapkit',
configuredBaseUrl?: string | null,
): string | undefined {
if (method === 'iapkit') {
return undefined;
}

const baseUrl = configuredBaseUrl?.trim();
if (!baseUrl) {
throw new Error(
'IAPKIT_BASE_URL not configured for Local (IAPKit) verification',
);
}

return baseUrl;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The method parameter is typed as 'iapkit-localhost' | 'iapkit', but the callers in PurchaseFlow.tsx and SubscriptionFlow.tsx pass currentVerificationMethod which is of the wider VerificationMethod type. This can cause TypeScript compilation errors due to type widening. Typing the parameter as string and checking method !== 'iapkit-localhost' avoids this issue.

Suggested change
export function resolveIapkitVerificationBaseUrl(
method: 'iapkit-localhost' | 'iapkit',
configuredBaseUrl?: string | null,
): string | undefined {
if (method === 'iapkit') {
return undefined;
}
const baseUrl = configuredBaseUrl?.trim();
if (!baseUrl) {
throw new Error(
'IAPKIT_BASE_URL not configured for Local (IAPKit) verification',
);
}
return baseUrl;
}
export function resolveIapkitVerificationBaseUrl(
method: string,
configuredBaseUrl?: string | null,
): string | undefined {
if (method !== 'iapkit-localhost') {
return undefined;
}
const baseUrl = configuredBaseUrl?.trim();
if (!baseUrl) {
throw new Error(
'IAPKIT_BASE_URL not configured for Local (IAPKit) verification',
);
}
return baseUrl;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this: the call occurs only in the else branch after ignore and local have both been excluded, so TypeScript narrows currentVerificationMethod to iapkit-localhost | iapkit. The strict React Native typecheck passes and includes both flows. Widening the helper to string would hide invalid calls and weaken its IAPKit-only contract.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 70 out of 83 changed files in this pull request and generated no new comments.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements support for local and self-hosted IAPKit receipt verification under OpenIAP Spec 2.3.1. It introduces a baseUrl property to RequestVerifyPurchaseWithIapkitProps across all supported platforms and frameworks (including Apple, Google, React Native, Expo, Flutter, Godot, KMP, and MAUI) to route verification requests to a custom local or self-hosted server. Additionally, it updates the React Native and Expo examples with a new 'Local (IAPKit)' verification option, implements strict origin validation, and adds E2E testing documentation. Feedback on the changes suggests replacing non-POSIX bash extensions ([[) with standard [ tests in the smoke-server.sh script to ensure portability on POSIX-compliant shells.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

sleep 0.25
done

if [[ "$ready" -ne 1 ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using [[ is a bash extension and is not supported by POSIX-compliant shells like dash (which is the default /bin/sh on Debian/Ubuntu systems). If this script is executed with /bin/sh, it will result in a syntax error. Consider using the standard [ command instead for better portability.

Suggested change
if [[ "$ready" -ne 1 ]]; then
if [ "$ready" -ne 1 ]; then

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this: smoke-server.sh is an explicit Bash executable (#!/usr/bin/env bash), all repository call sites execute it directly, and the script already depends on BASH_SOURCE, pipefail, local, and existing [[ expressions. Replacing one comparison would not make it POSIX sh compatible; bash -n passes.

-H "Content-Type: application/json" \
--data '{}' \
"http://localhost:${PORT}${path}")"
if [[ "$code" != "$expected" ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Similarly, using [[ here is a bash extension. Consider using the standard [ command for POSIX compatibility.

Suggested change
if [[ "$code" != "$expected" ]]; then
if [ "$code" != "$expected" ]; then

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this: smoke-server.sh is an explicit Bash executable (#!/usr/bin/env bash), all repository call sites execute it directly, and the script already depends on Bash-only features. Replacing this one [[ expression would not provide POSIX sh compatibility; bash -n passes.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for local and self-hosted IAPKit receipt verification (OpenIAP Spec 2.3.1) by adding a baseUrl parameter to RequestVerifyPurchaseWithIapkitProps across multiple platforms and frameworks, including Apple, Google, React Native, Expo, Flutter, Godot, KMP, and MAUI. It also adds a new E2E test guide for local IAPKit verification with the Martie example, updates documentation, and adds verification tests. The review feedback correctly identifies that the adb reverse command used in several scripts and documentation files does not support the --no-rebind option, which will cause execution errors and should be removed.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +110 to +115
elif adb -s "$ANDROID_SERIAL" reverse --no-rebind tcp:3100 tcp:3100; then
IAPKIT_REVERSE_CREATED=1
else
IAPKIT_REVERSE_CREATED=0
IAPKIT_REVERSE_BLOCKED=1
echo 'BLOCKED: could not create tcp:3100 reverse mapping without rebinding' >&2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The adb reverse command does not support the --no-rebind option (unlike adb forward). Running this command with --no-rebind will result in an error: unknown port specification '--no-rebind' and cause the script to fail. Since the script already checks if the port is reversed, you can safely run adb reverse without --no-rebind.

Suggested change
elif adb -s "$ANDROID_SERIAL" reverse --no-rebind tcp:3100 tcp:3100; then
IAPKIT_REVERSE_CREATED=1
else
IAPKIT_REVERSE_CREATED=0
IAPKIT_REVERSE_BLOCKED=1
echo 'BLOCKED: could not create tcp:3100 reverse mapping without rebinding' >&2
elif adb -s "$ANDROID_SERIAL" reverse tcp:3100 tcp:3100; then
IAPKIT_REVERSE_CREATED=1
else
IAPKIT_REVERSE_CREATED=0
IAPKIT_REVERSE_BLOCKED=1
echo 'BLOCKED: could not create tcp:3100 reverse mapping' >&2

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this: Android Platform Tools adb 35.0.2 documents reverse [--no-rebind] REMOTE LOCAL in adb help. The flag is intentional because a mapping can appear after the initial list check; plain reverse could overwrite it. This block instead fails closed when the non-rebinding create cannot proceed.

Comment on lines +126 to +131
elif adb -s "$ANDROID_SERIAL" reverse --no-rebind tcp:3100 tcp:3100; then
IAPKIT_REVERSE_CREATED=1
else
IAPKIT_REVERSE_CREATED=0
IAPKIT_REVERSE_BLOCKED=1
echo 'BLOCKED: could not create tcp:3100 reverse mapping without rebinding' >&2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The adb reverse command does not support the --no-rebind option. Running this command with --no-rebind will result in an error: unknown port specification '--no-rebind' and cause the script to fail. Since the script already checks if the port is reversed, you can safely run adb reverse without --no-rebind.

Suggested change
elif adb -s "$ANDROID_SERIAL" reverse --no-rebind tcp:3100 tcp:3100; then
IAPKIT_REVERSE_CREATED=1
else
IAPKIT_REVERSE_CREATED=0
IAPKIT_REVERSE_BLOCKED=1
echo 'BLOCKED: could not create tcp:3100 reverse mapping without rebinding' >&2
elif adb -s "$ANDROID_SERIAL" reverse tcp:3100 tcp:3100; then
IAPKIT_REVERSE_CREATED=1
else
IAPKIT_REVERSE_CREATED=0
IAPKIT_REVERSE_BLOCKED=1
echo 'BLOCKED: could not create tcp:3100 reverse mapping' >&2

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this: Android Platform Tools adb 35.0.2 documents reverse [--no-rebind] REMOTE LOCAL in adb help. The flag preserves the runbook's ownership rule across the list/bind race, and the block explicitly stops as BLOCKED if creation fails.

cp .env.example .env
```

For hosted IAPKit, get a key from the [IAPKit dashboard](https://kit.openiap.dev). Set `EXPO_PUBLIC_IAPKIT_API_KEY` to a key issued by the IAPKit/Convex deployment that the selected server uses. For **Local (IAPKit)**, the key and local server must target the same Convex deployment. Also set `EXPO_PUBLIC_IAPKIT_BASE_URL` to the device-reachable HTTP(S) origin only; do not append `/v1/purchase/verify`. A physical iPhone must use the Mac's LAN address. An Android device connected over USB can use `http://127.0.0.1:3100`: inspect `adb -s "$ANDROID_SERIAL" reverse --list`, reuse an exact `tcp:3100` mapping when present, or create it with `adb -s "$ANDROID_SERIAL" reverse --no-rebind tcp:3100 tcp:3100`. Record whether this run created the rule and remove only that rule during cleanup; if `--no-rebind` fails, use another port instead of overwriting an existing mapping.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The adb reverse command does not support the --no-rebind option. Please remove the --no-rebind references from the instructions to avoid confusion.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this: Android Platform Tools adb 35.0.2 explicitly supports reverse [--no-rebind] REMOTE LOCAL. Keeping --no-rebind prevents the example instructions from overwriting a mapping that appears between inspection and creation.

cp example/.env.example example/.env
```

For hosted IAPKit, get a key from the [IAPKit dashboard](https://kit.openiap.dev). Set `IAPKIT_API_KEY` to a key issued by the IAPKit/Convex deployment that the selected server uses. For **Local (IAPKit)**, the key and local server must target the same Convex deployment. Also set `IAPKIT_BASE_URL` to the device-reachable HTTP(S) origin only; do not append `/v1/purchase/verify`. A physical iPhone must use the Mac's LAN address. An Android device connected over USB can use `http://127.0.0.1:3100`: inspect `adb -s "$ANDROID_SERIAL" reverse --list`, reuse an exact `tcp:3100` mapping when present, or create it with `adb -s "$ANDROID_SERIAL" reverse --no-rebind tcp:3100 tcp:3100`. Record whether this run created the rule and remove only that rule during cleanup; if `--no-rebind` fails, use another port instead of overwriting an existing mapping.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The adb reverse command does not support the --no-rebind option. Please remove the --no-rebind references from the instructions to avoid confusion.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this: Android Platform Tools adb 35.0.2 explicitly supports reverse [--no-rebind] REMOTE LOCAL. Keeping --no-rebind prevents the example instructions from overwriting a mapping that appears between inspection and creation.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 73 out of 86 changed files in this pull request and generated 1 comment.

Comment on lines +4584 to +4589
/// <summary>Available in OpenIAP Spec 2.3.1 / openiap-apple 2.4.0 / openiap-google 2.4.0.</summary>
/// <summary>Base URL for the IAPKit server. Defaults to https://kit.openiap.dev.</summary>
/// <summary>Set this to a reachable HTTP(S) origin when self-hosting or testing a local IAPKit server.</summary>
/// <summary>The apiKey must be issued by the same IAPKit/Convex deployment as this server.</summary>
[JsonPropertyName("baseUrl")]
public string? BaseUrl { get; init; }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not changing this generated file: the C# generator intentionally emits one inline summary element per GraphQL description line, and the same established pattern appears throughout Types.cs. The MAUI net9/net10 builds run with GenerateDocumentationFile and TreatWarningsAsErrors and pass without XML-doc warnings. A repository-wide generator formatting redesign is separate from this API addition, and generated copies must not be edited directly.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements support for local and self-hosted IAPKit receipt verification across the entire OpenIAP ecosystem, updating the OpenIAP Spec to version 2.3.1. It introduces an optional baseUrl parameter to RequestVerifyPurchaseWithIapkitProps, allowing clients to route verification requests to a custom, device-reachable local server instead of the default hosted service. The changes span shared GQL schemas, native packages (openiap-apple, openiap-google), and all framework libraries (react-native-iap, expo-iap, flutter_inapp_purchase, godot-iap, kmp-iap, and OpenIap.Maui). Robust HTTP(S) origin validation has been added to prevent malformed URLs, and the React Native and Expo example apps have been updated with a new "Local (IAPKit)" verification option and comprehensive test suites. Additionally, E2E testing instructions and a new Claude skill have been added to guide developers through physical-device testing. There are no review comments to evaluate, and the implementation is clean and well-tested.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jul 13, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jul 13, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jul 13, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jul 13, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jul 13, 2026
@hyodotdev hyodotdev deleted a comment from coderabbitai Bot Jul 13, 2026
@hyochan
hyochan merged commit 9437311 into main Jul 13, 2026
23 checks passed
@hyochan
hyochan deleted the feat/iapkit-local-receipt-verification branch July 13, 2026 17:53

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for local and self-hosted IAPKit receipt verification across the OpenIAP specification (bumping it to version 2.3.1) and all supported framework libraries (React Native, Expo, Flutter, Godot, KMP, and .NET MAUI) as well as the native Apple and Google packages. It adds a new baseUrl property to the IAPKit verification properties, allowing clients to route verification requests to a custom local or self-hosted server origin instead of defaulting to the hosted service. Comprehensive validation of the custom base URL is implemented across platforms to ensure it is a valid HTTP(S) origin, with extensive unit and integration tests added. Additionally, the Martie React Native and Expo examples have been updated with a new Local (IAPKit) verification option and a custom modal selector, and the local server smoke tests have been hardened to verify port ownership and route handling. No review comments were provided for this pull request.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖 android Related to android cross-platform Cross-platform (both Android & iOS) 📖 documentation Improvements or additions to documentation 👀 example expo-iap expo-iap library 🎯 feature New feature flutter_inapp_purchase flutter_inapp_purchase library godot-iap godot-iap library 📱 iOS Related to iOS kit IAPKit (receipt-validation SaaS) kmp-iap kmp-iap library ⬡ protocol react-native-iap react-native-iap library 🧪 test Issue or pr related to testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants