Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/rn-build-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Verify Android build uses published native SDK
run: scripts/check_no_local_sdk_default

- name: Setup Node.js, pnpm, and install dependencies
uses: ./.github/actions/setup
with:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/rn-test-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Verify Android build uses published native SDK
run: scripts/check_no_local_sdk_default

- name: Setup Node.js, pnpm, and install dependencies
uses: ./.github/actions/setup
with:
Expand Down
89 changes: 54 additions & 35 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,61 +48,80 @@ uses the committed `Package.resolved` files instead of silently updating package
pins. This prevents sample app dependencies such as Apollo iOS from being
written into the repo-root Swift package lockfile.

## React Native development with local native SDK changes
## React Native and the native SDKs

Until the new native SDK libraries have stable released versions, assume React Native validation needs the local native SDK workflow. Use `--local` whenever running the React Native sample or native React Native tests that depend on the in-repo Swift/Kotlin SDKs.
**Default: do not pass `--local`, and do not set `USE_LOCAL_SDK=1`.** React Native builds
resolve the published native SDKs from CocoaPods and Maven Central. That is what CI does, and
it is what almost all React Native work needs.

Use the React Native `--local` workflow when you need to test React Native against native SDK changes that exist in this repository but have not been released as a SemVer/CocoaPods/Maven version yet.
### The question that decides it

This applies when changes are made under:
Does the native API you need already exist in the published version pinned at
`checkoutKit.nativeSdkVersions` in
`platforms/react-native/modules/@shopify/checkout-kit-react-native/package.json`?

- `platforms/swift/`the iOS Swift SDK / CocoaPods sources
- `platforms/android/` — the Android SDK / Maven artifact sources
- `protocol/languages/kotlin/`Kotlin protocol artifacts consumed by the Android SDK
- **Yes**do not use `--local`. Editing files under `platforms/swift/` or
`platforms/android/` does not on its own require it.
- **No**you are adding that API in this PR. Only then is `--local` correct.

It does **not** refer to the React Native wrapper platform folders:
### The one workflow that needs it

- `platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/`
- `platforms/react-native/modules/@shopify/checkout-kit-react-native/android/`
`--local` covers a single case: you changed the Swift or Kotlin **public API**, and you want to
integrate the React Native side against it now, before that native SDK version ships.

### What `--local` does
1. Make the Swift or Kotlin public API change and submit it in a PR.
2. Use `--local` to build the React Native side against those in-repo sources.

- For React Native iOS, `--local` wires CocoaPods to the in-repo `platforms/swift/` sources via a local path instead of a released pod version.
- For React Native Android, `--local` publishes/uses the in-repo Android SDK and Kotlin protocol artifacts through Maven Local so Gradle resolves the local `com.shopify:checkout-kit` and `com.shopify:embedded-checkout-protocol` artifacts instead of released Maven versions.
Expect this state while you do it:

### When to use it
- CI stays red and the PR is not mergeable. CI resolves published artifacts only. It does not
accept a `Podfile.lock` or a Maven resolution produced by `--local`.
- The PR becomes mergeable once the native release reaches CocoaPods and Maven Central and
`checkoutKit.nativeSdkVersions` is bumped to it.

Use `--local` whenever you are validating React Native behavior that depends on unreleased native SDK changes, for example:
This is a local development aid for early integration. It is not part of normal development.

- a new Swift SDK API that the React Native iOS bridge calls
- a new Android SDK API that the React Native Android bridge calls
- generated protocol/model changes under `protocol/languages/kotlin/` that the React Native module consumes through Android
- any change in `platforms/swift/`, `platforms/android/`, or `protocol/languages/kotlin/` that has not yet been released and consumed through normal dependency versions
### Scope

Re-run the relevant local workflow whenever `platforms/swift/`, `platforms/android/`, or `protocol/languages/kotlin/` changes, because the React Native sample/tests need to re-resolve those local native SDK sources/artifacts.
`--local` concerns the native SDK sources:

```bash
# iOS sample using local platforms/swift sources
dev rn ios --local
- `platforms/swift/` — the iOS Swift SDK / CocoaPods sources
- `platforms/android/` — the Android SDK / Maven artifact sources
- `protocol/languages/kotlin/` — Kotlin protocol artifacts consumed by the Android SDK

# Android sample using local Android and Kotlin protocol artifacts via Maven Local
dev rn android --local
It does **not** concern the React Native wrapper platform folders, which build from source
either way:

# React Native Android unit tests using local Android and Kotlin protocol artifacts via Maven Local
# `dev rn test android` publishes the Android SDK artifacts to ~/.m2 first, then runs the RN module tests.
dev rn test android
```
- `platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/`
- `platforms/react-native/modules/@shopify/checkout-kit-react-native/android/`

### What it does

For ad-hoc Android Gradle test commands, publish the local Android SDK first and set `USE_LOCAL_SDK=1` so the React Native sample resolves the local `com.shopify:checkout-kit` and `com.shopify:embedded-checkout-protocol` artifacts from Maven Local:
- iOS: wires CocoaPods to the in-repo `platforms/swift/` sources via a local path instead of a released pod version.
- Android: publishes the in-repo Android SDK and Kotlin protocol artifacts to Maven Local, then resolves `com.shopify:checkout-kit` and `com.shopify:embedded-checkout-protocol` from there.

```bash
cd platforms/react-native
USE_LOCAL_SDK=1 ./scripts/publish_android_snapshot
cd sample/android
USE_LOCAL_SDK=1 ./gradlew :shopify_checkout-kit-react-native:testDebugUnitTest
dev rn ios --local
dev rn android --local
dev rn test android --local
```

The React Native Android sample uses exclusive Maven Local resolution for those two `com.shopify` modules when `USE_LOCAL_SDK=1`. Keep that filtering in the sample Gradle build; duplicating exclusive repository filters for the same modules elsewhere can break dependency resolution.
Re-run the relevant command whenever `platforms/swift/`, `platforms/android/`, or
`protocol/languages/kotlin/` changes, so the build re-resolves those sources.

### Rules

- Never commit a `Podfile.lock` generated with `--local`. It records a local path, and
`platforms/react-native/scripts/check_published_podfile_lock` fails CI on it. Regenerate with
`env -u USE_LOCAL_SDK dev rn pod-install`.
- Never hardcode `USE_LOCAL_SDK=1` into a script, `dev.yml`, or a workflow. It has to stay an
explicit choice made on the command line, or local runs stop matching CI and can resolve a
stale artifact from `~/.m2`. `platforms/react-native/scripts/check_no_local_sdk_default` fails
CI on it.
- The React Native Android sample uses exclusive Maven Local resolution for those two
`com.shopify` modules when `USE_LOCAL_SDK=1`. Keep that filtering in the sample Gradle build;
duplicating exclusive repository filters for the same modules elsewhere can break dependency
resolution.

## Sensitive configuration

Expand Down
38 changes: 30 additions & 8 deletions dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -494,13 +494,16 @@ commands:
Runs unit tests across all three React Native targets:
- JS: Jest tests in `platforms/react-native/modules/@shopify/checkout-kit-react-native/tests/`
- iOS: Xcode/CocoaPods tests in `platforms/react-native/test/rct-integration-app/`
- Android: Gradle JVM tests for `:shopify_checkout-kit-react-native` (requires a local Maven publish of `:lib`)
- Android: Gradle JVM tests for `:shopify_checkout-kit-react-native`

All three targets resolve the published native SDKs, matching CI. To test
against unreleased native SDK changes, run `dev rn test android --local`.
run: |
set -e
cd platforms/react-native && pnpm test
pnpm sample test:ios
USE_LOCAL_SDK=1 ./scripts/publish_android_snapshot
cd sample/android && USE_LOCAL_SDK=1 ./gradlew :shopify_checkout-kit-react-native:test --refresh-dependencies
./scripts/publish_android_snapshot
cd sample/android && ./gradlew :shopify_checkout-kit-react-native:test --refresh-dependencies
subcommands:
js:
desc: Run JS unit tests via jest
Expand All @@ -516,13 +519,27 @@ commands:
optional: --local
run: cd platforms/react-native && pnpm sample test:ios "$@"
android:
desc: Run native Android unit tests for the RN module (publishes/uses local platforms/android SDK)
desc: Run native Android unit tests for the RN module against the published Android SDK
long_desc: |
Resolves the published `com.shopify:checkout-kit` version pinned at
`checkoutKit.nativeSdkVersions.android` in the React Native module, matching CI.

--local
Resolve the in-repo Android SDK from Maven Local instead. Only for
unreleased native API that this PR changes.
syntax:
optional: --local
run: |
set -e
cd platforms/react-native
USE_LOCAL_SDK=1 ./scripts/publish_android_snapshot
for arg in "$@"; do
if [ "$arg" = "--local" ]; then
export USE_LOCAL_SDK=1
fi
done
./scripts/publish_android_snapshot
cd sample/android
USE_LOCAL_SDK=1 ./gradlew :shopify_checkout-kit-react-native:test --refresh-dependencies
./gradlew :shopify_checkout-kit-react-native:test --refresh-dependencies
e2e:
desc: Run React Native sample Maestro checkout smoke flows
syntax: "{ios|android} [--guest] [--hardcoded-buyer-identity]"
Expand Down Expand Up @@ -642,7 +659,8 @@ commands:
Ensures CocoaPods are in sync, then builds and runs the iOS sample app on a simulator.

--local
Build against in-repo Swift SDK sources.
Build against in-repo Swift SDK sources. Only for unreleased native API that
this PR adds; CI resolves published pods, so the PR stays red until release.
syntax:
optional: --local
run: cd platforms/react-native && pnpm sample ios "$@"
Expand All @@ -654,6 +672,8 @@ commands:

--local
Build against in-repo SDK sources (publishes a local Maven snapshot first).
Only for unreleased native API that this PR adds; CI resolves published Maven
artifacts, so the PR stays red until release.
syntax:
optional: --local
run: cd platforms/react-native && pnpm sample android "$@"
Expand All @@ -664,7 +684,9 @@ commands:
Runs pod install for the iOS sample app.

--local
Wire the Podfile against in-repo Swift SDK sources.
Wire the Podfile against in-repo Swift SDK sources. Only for unreleased native
API that this PR adds. Never commit the Podfile.lock this produces; regenerate
it with `env -u USE_LOCAL_SDK dev rn pod-install`.
syntax:
optional: --local
run: cd platforms/react-native && pnpm run pod-install -- "$@"
Expand Down
22 changes: 14 additions & 8 deletions platforms/react-native/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,30 +57,36 @@ pods need to be installed. The underlying

## Local SDK development (`--local`)

The RN module wraps the Shopify Swift and Android SDKs, which live in this same monorepo at `platforms/swift/` and `platforms/android/`. By default the sample app builds against the **published** artifacts on CocoaPods / Maven Central — the same path CI takes. To build against the in-repo SDK sources instead, pass `--local` to any sample command:
The RN module wraps the Shopify Swift and Android SDKs, which live in this same monorepo at `platforms/swift/` and `platforms/android/`. The sample app builds against the **published** artifacts on CocoaPods / Maven Central — the same path CI takes.

**Most work does not need `--local`.** Before reaching for it, check whether the native API you need already exists in the published version pinned at `checkoutKit.nativeSdkVersions` in `modules/@shopify/checkout-kit-react-native/package.json`. If it does, build normally. Editing `platforms/swift/` or `platforms/android/` does not on its own require the flag.

`--local` is for one case: you changed the Swift or Kotlin **public API** and want to integrate the RN side against it before that native version ships.

```sh
dev rn android --local # publishes lib to ~/.m2/ then builds the sample against it
dev rn ios --local # builds the sample against the local Swift SDK
dev rn pod-install --local # re-resolve iOS pods against the local Swift SDK
dev rn test android --local # RN module Android tests against the local Android SDK
```

The flag is opt-in because the in-repo SDKs change as we develop. Default published mode is always safe; `--local` activates the in-progress API surface.
While you do this, CI stays red and the PR is not mergeable — CI resolves published artifacts only. It becomes mergeable when the native release lands on CocoaPods and Maven Central and `nativeSdkVersions` is bumped to it. That red state is expected, not a problem to work around.

### How it works

- **Android**: every `--local` invocation runs `scripts/publish_android_snapshot`, which publishes the current local `com.shopify:embedded-checkout-protocol` and `com.shopify:checkout-kit` versions to `~/.m2/` via the Android Gradle build. The sample's `build.gradle` uses exclusive Maven Local resolution for those modules, so validation fails rather than falling back to a published artifact if the local publish is missing.
- **iOS**: with `--local`, the Podfile injects `pod "ShopifyCheckoutKit", :path => "../../../../"` (the repo root, where `ShopifyCheckoutKit.podspec` lives). CocoaPods reads Swift sources from `platforms/swift/` directly.

Internally `--local` exports `USE_LOCAL_SDK=1` before invoking the underlying tool. Setting the env var directly works too:

```sh
USE_LOCAL_SDK=1 dev rn android
```
Internally `--local` exports `USE_LOCAL_SDK=1` before invoking the underlying tool. Pass the flag rather than exporting the variable in your shell profile — an ambient `USE_LOCAL_SDK=1` silently applies to every later build.

### CI

CI uses the default (published) path naturally — no special flag handling. Keep `USE_LOCAL_SDK=1` scoped to local development or explicit validation against unreleased native SDK changes.
CI uses the published path and never sets the flag. Two guards keep it that way:

- `scripts/check_published_podfile_lock` fails the iOS jobs if a committed `Podfile.lock` resolves `ShopifyCheckoutKit` from a local path. Regenerate with `env -u USE_LOCAL_SDK dev rn pod-install`.
- `scripts/check_no_local_sdk_default` fails the Android jobs if `USE_LOCAL_SDK=1` is hardcoded into a script, `dev.yml`, or a workflow, or if `mavenLocal()` is added outside the `useLocalSdk` branch. The sample Gradle build also aborts when the variable is set while `CI` is set.

Local mode has to stay an explicit command-line choice. Hardcoding it makes local runs disagree with CI, and because Maven Local keeps the same version across republishes, it can quietly resolve a stale artifact from `~/.m2`.

### Gotchas

Expand Down
7 changes: 7 additions & 0 deletions platforms/react-native/sample/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ apply plugin: "com.facebook.react.rootproject"

def useLocalSdk = (System.getenv("USE_LOCAL_SDK") ?: "0") == "1"

if (useLocalSdk && System.getenv("CI") != null) {
throw new GradleException(
"USE_LOCAL_SDK=1 is set in CI. CI must resolve the published native SDKs, " +
"so this build would not match what merges. Unset USE_LOCAL_SDK."
)
}

allprojects {
repositories {
if (useLocalSdk) {
Expand Down
23 changes: 23 additions & 0 deletions platforms/react-native/scripts/check_no_local_sdk_default
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

REPO_ROOT = File.expand_path("../../..", __dir__)

require_relative "#{REPO_ROOT}/scripts/lib/local_sdk_default"

problems = LocalSdkDefault.problems_by_path(REPO_ROOT)

if problems.any?
problems.each do |path, found|
found.each do |problem|
warn "::error file=#{path}::#{problem}"
end
end
warn ""
warn "Local SDK mode must stay opt-in. CI resolves published native SDKs, so a hardcoded"
warn "USE_LOCAL_SDK=1 makes local runs disagree with CI and can resolve a stale ~/.m2 artifact."
warn "Pass --local at the command line instead."
exit 1
end

puts "No hardcoded local SDK defaults; React Native builds resolve published native SDKs."
62 changes: 62 additions & 0 deletions scripts/lib/local_sdk_default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

class LocalSdkDefault
ENABLES_LOCAL_SDK = /(?:\A|[\s&;(])(?:export\s+)?USE_LOCAL_SDK=1\b/
ADDS_MAVEN_LOCAL = /\bmavenLocal\s*\(/
LOCAL_FLAG_GUARD = /--local/
USE_LOCAL_SDK_GUARD = /\buseLocalSdk\b/
LOCAL_FLAG_LOOKBEHIND = 3
USE_LOCAL_SDK_LOOKBEHIND = 10

SCANNED_PATHSPECS = [
"dev.yml",
".github/workflows",
"platforms/android",
"platforms/react-native",
].freeze

EXCLUDED_PATHS = ["platforms/react-native/scripts/check_no_local_sdk_default"].freeze

attr_reader :path

def self.scanned_paths(repo_root)
Dir.chdir(repo_root) do
`git ls-files -z -- #{SCANNED_PATHSPECS.join(" ")}`
.split("\0")
.reject { |relative| relative.end_with?(".md") || EXCLUDED_PATHS.include?(relative) }
end
end

def self.problems_by_path(repo_root)
scanned_paths(repo_root).each_with_object({}) do |relative, memo|
contents = File.read(File.join(repo_root, relative), encoding: "UTF-8")
next unless contents.valid_encoding?

problems = new(relative, contents).problems
memo[relative] = problems unless problems.empty?
rescue Errno::ENOENT
next
end
end

def initialize(path, contents)
@path = path
@lines = contents.lines.map(&:chomp)
end

def problems
@lines.each_with_index.filter_map do |line, index|
if ENABLES_LOCAL_SDK.match?(line) && !guarded?(index, LOCAL_FLAG_GUARD, LOCAL_FLAG_LOOKBEHIND)
"line #{index + 1} sets USE_LOCAL_SDK=1 without a --local opt-in guard"
elsif ADDS_MAVEN_LOCAL.match?(line) && !guarded?(index, USE_LOCAL_SDK_GUARD, USE_LOCAL_SDK_LOOKBEHIND)
"line #{index + 1} adds mavenLocal() without a useLocalSdk guard"
end
end
end

private

def guarded?(index, pattern, lookbehind)
@lines[[index - lookbehind, 0].max..index].any? { |line| pattern.match?(line) }
end
end
Loading