diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 9d267f83..ae63d3ed 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -39,6 +39,17 @@ When in doubt about whether we will be interested in including a new feature, pl 7. Ensure all checks (tests, lint) are passing in GitHub. 8. Open a pull request with a detailed description of what is changing and why. +### Dev tooling + +Shopify employees can use the root `dev.yml` from the repo root: + +```bash +dev up +dev check +``` + +Platform-scoped commands are available as `dev android `, `dev swift `, and `dev react-native ` (or `dev rn`). Protocol schema/model commands are available as `dev protocol `. For cross-platform changes, use `dev lint`, `dev test`, `dev check`, `dev format`, and `dev build`. + --- ## Swift (`platforms/swift/`) diff --git a/AGENTS.md b/AGENTS.md index a43da5ea..f7eee896 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,31 @@ e2e/ # cross-platform end-to-end tests .github/ # workflows, issue templates, CODEOWNERS ``` +## Dev workflow + +> **AI agents:** All commands require the `shadowenv exec --` prefix to run inside the shadowenv-managed environment. +> +> ``` +> shadowenv exec --dir -- /opt/dev/bin/dev up +> shadowenv exec --dir -- /opt/dev/bin/dev test [ARGS] +> ``` + +Run `dev` commands from the repo root. Use `dev up` before running commands when +the environment may not be provisioned. + +For platform-scoped work, prefer the root `dev.yml` commands: + +- Android: `dev android ` +- Swift: `dev swift ` +- React Native: `dev react-native ` or `dev rn ` + +For protocol schema/model work, use `dev protocol `. + +For cross-platform changes, use the repo-wide aggregates: `dev lint`, +`dev test`, `dev check`, `dev format`, and `dev build`. Use +`dev format` for formatting; `fix` remains an alias for existing +workflows. + ## React Native development with local native SDK changes 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. diff --git a/dev.yml b/dev.yml index d336810b..d5198c5e 100644 --- a/dev.yml +++ b/dev.yml @@ -8,6 +8,7 @@ up: - xcbeautify - jq - swiftlint + - swiftformat - sccache - ruby - custom: @@ -88,6 +89,15 @@ check: commands: # Repo-wide + build: + desc: Build all supported workspaces + run: | + set -e + /opt/dev/bin/dev android build + /opt/dev/bin/dev swift build + /opt/dev/bin/dev react-native build + /opt/dev/bin/dev web build + codegen: desc: "Generate UCP models. Usage: dev codegen " syntax: "" @@ -96,6 +106,36 @@ commands: kotlin|swift|typescript|ts) ./protocol/scripts/generate_models.sh --lang "$1" ;; *) echo "Usage: dev codegen "; exit 1 ;; esac + + format: + desc: Auto-format and apply safe lint autocorrections across supported workspaces + aliases: [fix] + run: | + set -e + /opt/dev/bin/dev android format + /opt/dev/bin/dev swift format + /opt/dev/bin/dev react-native format + /opt/dev/bin/dev web format + + lint: + desc: Run lint checks across supported workspaces + aliases: [style] + run: | + set -e + /opt/dev/bin/dev android lint + /opt/dev/bin/dev swift lint + /opt/dev/bin/dev react-native lint + /opt/dev/bin/dev web lint + + test: + desc: Run tests across all supported workspaces + run: | + set -e + /opt/dev/bin/dev android test + /opt/dev/bin/dev swift test + /opt/dev/bin/dev react-native test + /opt/dev/bin/dev web test + apollo: subcommands: download_schema: @@ -117,6 +157,23 @@ commands: *) echo "Usage: dev apollo codegen [accelerated|mobile-buy|all]"; exit 1 ;; esac + # Protocol + protocol: + desc: "Checkout protocol commands" + subcommands: + build: + desc: Build the Swift protocol target + run: cd protocol/languages/swift && swift build + test: + desc: Build the Swift protocol test target + run: cd protocol/languages/swift && swift build --build-tests + check: + desc: Build the Swift protocol target and test target + run: | + set -e + /opt/dev/bin/dev protocol build + /opt/dev/bin/dev protocol test + # Android android: desc: "Android Checkout Kit commands" @@ -129,6 +186,10 @@ commands: desc: Build all sample applications run: cd platforms/android/samples/MobileBuyIntegration && ./gradlew build + clean: + desc: Clean Android Gradle build outputs + run: cd platforms/android && ./gradlew clean + test: desc: Run all tests with clean build run: cd platforms/android && ./gradlew clean test --console=plain @@ -143,8 +204,9 @@ commands: aliases: [style] run: cd platforms/android && ./gradlew detekt lintRelease - fix: - desc: Automatically fix format and lint issues where possible + format: + desc: Auto-format and apply safe lint autocorrections + aliases: [fix] run: cd platforms/android && ./gradlew detekt --auto-correct api: @@ -154,6 +216,7 @@ commands: echo "" echo " check Verify public API matches the committed baseline" echo " dump Regenerate the baseline after intentional public API changes" + exit 1 subcommands: check: desc: Verify public API matches the committed baseline @@ -163,12 +226,11 @@ commands: run: cd platforms/android && ./gradlew :lib:apiDump check: - desc: Run all Android checks (detekt, android lint) + desc: Run all Android checks (detekt, Android lint) run: | set -e - cd platforms/android - ./gradlew detekt - ./gradlew lintRelease + /opt/dev/bin/dev android check detekt + /opt/dev/bin/dev android check android-lint subcommands: detekt: desc: Run detekt static analysis @@ -185,20 +247,39 @@ commands: desc: Check format and lint issues using SwiftLint and SwiftFormat aliases: [style] run: cd platforms/swift && ./Scripts/lint - fix: - desc: Automatically fix format and lint issues where possible + format: + desc: Auto-format and apply safe lint autocorrections + aliases: [fix] run: cd platforms/swift && ./Scripts/lint fix + clean: + desc: Clean Swift packages and sample app build artifacts + run: | + set -e + cd platforms/swift + # ShopifyCheckoutKit-Package is the SPM-wide scheme: it cleans all + # library targets in Package.swift (ShopifyCheckoutKit, + # ShopifyAcceleratedCheckouts, ShopifyCheckoutProtocol) in one pass. + ./Scripts/xcode_run clean ShopifyCheckoutKit-Package + cd Samples + # Sample apps have a "Run Script" build phase that runs during clean + # and exits non-zero when there is nothing to delete. Tolerate it so + # the second sample still gets cleaned. + ../Scripts/xcode_run clean MobileBuyIntegration || true + ../Scripts/xcode_run clean ShopifyAcceleratedCheckoutsApp || true build: - desc: Build Swift packages or sample apps + desc: Build all Swift packages and sample apps run: | - echo "Usage: dev swift build {packages|samples}" + set -e + cd platforms/swift + # ShopifyCheckoutKit-Package builds every library target in + # Package.swift in one xcodebuild invocation. The sample apps act as + # integration compilation checks against the built libraries. + ./Scripts/xcode_run build ShopifyCheckoutKit-Package + ./Scripts/build_samples subcommands: packages: - desc: Build both ShopifyCheckoutKit and ShopifyAcceleratedCheckouts packages - run: | - cd platforms/swift - ./Scripts/xcode_run build ShopifyCheckoutKit - ./Scripts/xcode_run build ShopifyAcceleratedCheckouts + desc: Build Swift package targets + run: cd platforms/swift && ./Scripts/xcode_run build ShopifyCheckoutKit-Package samples: desc: Build all sample applications to verify integration run: cd platforms/swift && ./Scripts/build_samples @@ -215,6 +296,7 @@ commands: echo "" echo " check Verify public Swift API matches the committed baselines" echo " dump Regenerate the Swift API baselines after intentional public API changes" + exit 1 subcommands: check: desc: Verify public Swift API matches the committed baselines @@ -222,12 +304,18 @@ commands: dump: desc: Regenerate the Swift API baselines after intentional public API changes run: cd platforms/swift && ./Scripts/api dump + check: + desc: Run Swift lint checks + run: /opt/dev/bin/dev swift lint # React Native react-native: desc: "React Native Checkout Kit commands" aliases: [rn] subcommands: + build: + desc: Build the @shopify/checkout-kit-react-native module + run: cd platforms/react-native && pnpm module build server: desc: Start Metro development server aliases: [s] @@ -273,9 +361,6 @@ commands: sccache --stop-server 2>/dev/null || true fi echo "Cleaned root, module and sample workspaces" - build: - desc: Build the @shopify/checkout-kit-react-native module - run: cd platforms/react-native && pnpm module build test: desc: Run React Native module tests (JS + iOS + Android) long_desc: | @@ -319,13 +404,13 @@ commands: desc: Lint Swift code via SwiftLint run: cd platforms/react-native && ./scripts/lint_swift module: - desc: Lint the @shopify/checkout-sheet-kit module + desc: Lint the @shopify/checkout-kit-react-native module run: cd platforms/react-native && pnpm module lint sample: desc: Lint the sample app run: cd platforms/react-native && pnpm sample lint format: - desc: Auto-fix Swift lint and format issues + desc: Auto-format and apply safe lint autocorrections (Swift bridge only) aliases: [fix] run: cd platforms/react-native && ./scripts/lint_swift fix api: @@ -335,6 +420,7 @@ commands: echo "" echo " check Verify public RN API matches the committed report" echo " dump Regenerate the RN API report after intentional public API changes" + exit 1 subcommands: check: desc: Verify public RN API matches the committed report @@ -342,6 +428,18 @@ commands: dump: desc: Regenerate the RN API report after intentional public API changes run: cd platforms/react-native && pnpm module api:dump + check: + desc: Run React Native lint checks + run: /opt/dev/bin/dev react-native lint + + rn: + desc: "Alias for React Native Checkout Kit commands" + long_desc: | + Alias for `dev react-native ...`. Use `dev help react-native` for the + full subcommand list. + syntax: + optional: "[command] [args]" + run: /opt/dev/bin/dev react-native "$@" # Web web: diff --git a/platforms/android/AGENTS.md b/platforms/android/AGENTS.md index 307a18a6..fbfa8823 100644 --- a/platforms/android/AGENTS.md +++ b/platforms/android/AGENTS.md @@ -41,7 +41,6 @@ The sample is a separate Gradle composite (`samples/MobileBuyIntegration/setting - **`-Xexplicit-api=strict`** is on (`lib/build.gradle`). Every public class, method, field, and property must have an explicit visibility modifier. "Accidentally public" is not a thing here. This is a consumer-protection rule — if you see a public-by-default declaration, it was deliberate. - **Max line length: 140** (detekt-enforced). Detekt config: `lib/detekt.config.yml`. -- **MIT license header required on every new source file.** Format: copy the top comment of any existing `.kt` or `.java` file in `lib/src/main` or `lib/src/test`. Enforced in CI via the repo-root `scripts/check_license_headers.rb`. - **Library JVM target: 11.** Consumers must build with JDK 11+ to consume the AAR. Raising further is a major-version discussion. - **Library Kotlin `apiVersion` / `languageVersion` are pinned at 2.0.** Set in `lib/build.gradle` so the AAR's bytecode stays consumable by Kotlin 2.0+ projects even though the compiler itself is on a newer 2.x. Bumping this pin is the consumer-facing breaking change, not bumping the compiler - treat it as a planned major-version event. - **Prefer generated protocol models.** Before adding hand-written protocol DTOs, check the generated models in `lib/src/main/java/com/shopify/checkoutkit/Models.kt` and the OpenRPC schema. Use generated UCP/ECP types for wire payloads; reserve local DTOs for Android-internal transport helpers that are not represented in the schema. @@ -63,7 +62,7 @@ If `apiCheck` fails and you did *not* intend to change public API, the diff tell - Tests: `./gradlew :lib:test` (or `dev android test`) - API surface: `./gradlew :lib:apiCheck` / `./gradlew :lib:apiDump` (or `dev android api check` / `dev android api dump`) - Lint: `./gradlew detekt lintRelease` (or `dev android lint`) -- Auto-fix lint: `./gradlew detekt --auto-correct` (or `dev android fix`) +- Format: `./gradlew detekt --auto-correct` (or `dev android format`) - Full local verification: `./gradlew :lib:clean :lib:test :lib:detekt :lib:lintRelease :lib:assembleRelease` - Sample app build (from `samples/MobileBuyIntegration/`): `./gradlew assembleDebug` diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/AcceleratedCheckoutButtons.swift b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/AcceleratedCheckoutButtons.swift index c1c9f5f5..3dfa0edd 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/AcceleratedCheckoutButtons.swift +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/ios/AcceleratedCheckoutButtons.swift @@ -137,8 +137,8 @@ class RCTAcceleratedCheckoutButtonsView: UIView { hostingController?.view.frame = bounds } - // Deprecated in iOS 17 — superseded by registerForTraitChanges in setupView(). - // Remove this override when dropping iOS 16 support. + /// Deprecated in iOS 17 — superseded by registerForTraitChanges in setupView(). + /// Remove this override when dropping iOS 16 support. override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) if #unavailable(iOS 17.0) { diff --git a/platforms/react-native/sample/src/screens/SettingsScreen.tsx b/platforms/react-native/sample/src/screens/SettingsScreen.tsx index 7363ee9f..24d4eba1 100644 --- a/platforms/react-native/sample/src/screens/SettingsScreen.tsx +++ b/platforms/react-native/sample/src/screens/SettingsScreen.tsx @@ -338,7 +338,7 @@ function BuyerIdentityDetails({ if (authenticated) { return ( - + Changing Buyer Identity will log you out. @@ -426,6 +426,9 @@ function createStyles(colors: Colors) { fontSize: 12, color: colors.textSubdued, }, + warningText: { + color: '#f5a623', + }, detailRow: { flexDirection: 'row', alignItems: 'center', diff --git a/platforms/react-native/scripts/lint_swift b/platforms/react-native/scripts/lint_swift index d909a495..f13e2feb 100755 --- a/platforms/react-native/scripts/lint_swift +++ b/platforms/react-native/scripts/lint_swift @@ -2,6 +2,9 @@ DIR=modules/@shopify/checkout-kit-react-native MODE="${1:-check}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)" +SWIFT_TOOLS_DIR="$REPO_ROOT/platforms/swift" # Validate the mode if [[ "$MODE" != "check" && "$MODE" != "fix" ]]; then @@ -16,20 +19,32 @@ fi print_install_instructions() { echo "🔧 FIX:" echo " Shopify employee? Run 'dev up'" - echo " Not a Shopify employee? Install via homebrew:" - echo " - SwiftLint: 'brew install swiftlint' / https://github.com/realm/SwiftLint" - echo " - SwiftFormat: 'brew install swiftformat' / https://github.com/nicklockwood/SwiftFormat" + echo " Not a Shopify employee? Install Mint and run 'mint bootstrap' from platforms/swift" } -# Check for SwiftLint -if ! which swiftlint >/dev/null; then +resolve_tool() { + local command_name=$1 + + if which "$command_name" >/dev/null; then + which "$command_name" + return 0 + fi + + if command -v mint >/dev/null; then + (cd "$SWIFT_TOOLS_DIR" && mint which "$command_name") + return $? + fi + + return 1 +} + +if ! SWIFTLINT="$(resolve_tool swiftlint)"; then echo "⚠️ WARN: SwiftLint not installed" print_install_instructions exit 1 fi -# Check for SwiftFormat -if ! which swiftformat >/dev/null; then +if ! SWIFTFORMAT="$(resolve_tool swiftformat)"; then echo "⚠️ WARN: SwiftFormat not installed" print_install_instructions exit 1 @@ -38,11 +53,11 @@ fi # Run SwiftLint if [[ "$MODE" == "check" ]]; then echo "🔄 Running SwiftLint in check mode..." - swiftlint lint --strict $DIR --config .swiftlint.yml + "$SWIFTLINT" lint --strict $DIR --config .swiftlint.yml LINT_STATUS=$? else echo "🔄 Running SwiftLint in fix mode..." - swiftlint lint --fix $DIR --config .swiftlint.yml + "$SWIFTLINT" lint --fix $DIR --config .swiftlint.yml LINT_STATUS=$? fi echo "SwiftLint exit status: $LINT_STATUS" @@ -50,11 +65,11 @@ echo "SwiftLint exit status: $LINT_STATUS" # Run SwiftFormat if [[ "$MODE" == "check" ]]; then echo "🔄 Running SwiftFormat in check mode..." - swiftformat $DIR --lint --config .swiftformat + "$SWIFTFORMAT" $DIR --lint --config .swiftformat FORMAT_STATUS=$? else echo "🔄 Running SwiftFormat in fix mode..." - swiftformat $DIR --config .swiftformat + "$SWIFTFORMAT" $DIR --config .swiftformat FORMAT_STATUS=$? fi echo "SwiftFormat exit status: $FORMAT_STATUS" diff --git a/platforms/swift/.cursor/rules/swift-development.mdc b/platforms/swift/.cursor/rules/swift-development.mdc index 5ae1c7d3..ccb49685 100644 --- a/platforms/swift/.cursor/rules/swift-development.mdc +++ b/platforms/swift/.cursor/rules/swift-development.mdc @@ -25,8 +25,8 @@ alwaysApply: true Key commands for verification: - `dev swift lint` (alias: `dev swift style`) - Check format & lint issues -- `dev swift fix` - Auto-fix formatting and linting issues -- `dev swift build packages` - Build both packages +- `dev swift format` - Auto-format and apply safe lint autocorrections +- `dev swift build packages` - Build Swift package targets - `dev swift test` - Run all tests ## Concurrency Best Practices @@ -50,7 +50,7 @@ actor QueryCache { ✅ DO: Run the appropriate dev command to verify ### After making changes: -- ALWAYS run `dev swift fix && dev swift lint` to check your code is formatted and written in our style. +- ALWAYS run `dev swift format && dev swift lint` to check your code is formatted and written in our style. ## Example Workflow @@ -58,7 +58,7 @@ actor QueryCache { # Make your Swift changes... # Fix any auto-fixable issues -dev swift fix +dev swift format # Check for any remaining lint issues dev swift lint diff --git a/platforms/swift/Samples/MobileBuyIntegration/README.md b/platforms/swift/Samples/MobileBuyIntegration/README.md index 56bed395..4f5c3de0 100644 --- a/platforms/swift/Samples/MobileBuyIntegration/README.md +++ b/platforms/swift/Samples/MobileBuyIntegration/README.md @@ -123,6 +123,19 @@ Open the project in Xcode, let Swift Package Manager resolve dependencies, then 5. Build in Xcode and fix any compile errors from schema changes. +## Dev commands reference + +All commands are run from the **repo root** (`checkout-kit/`): + +| Command | Description | +|---------|-------------| +| `dev apollo download_schema swift mobile-buy` | Download the Storefront API schema for this sample app | +| `dev apollo codegen swift mobile-buy` | Regenerate Swift types from `.graphql` files | +| `dev apollo codegen swift all` | Regenerate for all sample apps | +| `dev swift lint` | Run SwiftLint + SwiftFormat checks | +| `dev swift format` | Auto-format and apply safe lint autocorrections | +| `dev swift build samples` | Build all sample apps | + ## Key files | File | Purpose | diff --git a/platforms/swift/Scripts/apollo_codegen b/platforms/swift/Scripts/apollo_codegen index 9839c390..489324a3 100755 --- a/platforms/swift/Scripts/apollo_codegen +++ b/platforms/swift/Scripts/apollo_codegen @@ -23,7 +23,7 @@ run_codegen() { local schema_count=$(find "$REPO_ROOT/$dir" -name "*.graphqls" -maxdepth 1 | wc -l | tr -d ' ') if [ "$schema_count" -eq 0 ]; then echo "❌ No .graphqls schema file found in $dir" - echo " Run 'dev apollo download_schema $name' first" + echo " Run 'dev apollo download_schema swift $name' first" return 1 fi @@ -58,4 +58,4 @@ case "$APP" in ;; esac -/opt/dev/bin/dev swift fix +/opt/dev/bin/dev swift format diff --git a/platforms/swift/Scripts/lint b/platforms/swift/Scripts/lint index 22b82d4b..41e38bc5 100755 --- a/platforms/swift/Scripts/lint +++ b/platforms/swift/Scripts/lint @@ -138,7 +138,7 @@ print_linting_error() { echo "❌ $tool_name detected issues that need to be fixed." if [[ "$MODE" == "check" ]]; then echo "🔧 How to fix:" - echo " Shopify employee? Run 'dev swift fix' and resolve remaining issues" + echo " Shopify employee? Run 'dev swift format' and resolve remaining issues" echo " Not a Shopify employee? Run './Scripts/lint fix' and resolve remaining issues" else echo "🔧 These files will need to be fixed manually" @@ -169,6 +169,7 @@ if [[ "$MODE" == "check" && "$SKIP_POD" == "false" ]]; then # platforms/swift/ and protocol/languages/swift/. Run lint from the # repo root. REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)" + export BUNDLE_GEMFILE="${BUNDLE_GEMFILE:-$REPO_ROOT/platforms/swift/Gemfile}" POD_LOG="" if [[ "$VERBOSE" == "true" ]]; then (cd "$REPO_ROOT" && bundle exec pod lib lint ShopifyCheckoutKit.podspec --allow-warnings) @@ -188,7 +189,7 @@ if [[ "$MODE" == "check" && "$SKIP_POD" == "false" ]]; then echo "❌ CocoaPods lint exit status: $POD_STATUS" echo "❌ CocoaPods detected issues that need to be fixed." - echo "🔧 Run 'bundle exec pod lib lint ShopifyCheckoutKit.podspec --allow-warnings --verbose' from the repo root for details" + echo "🔧 Run 'BUNDLE_GEMFILE=platforms/swift/Gemfile bundle exec pod lib lint ShopifyCheckoutKit.podspec --allow-warnings --verbose' from the repo root for details" exit 1 fi diff --git a/platforms/swift/Scripts/xcode_run b/platforms/swift/Scripts/xcode_run index 7b45019e..1b9cf030 100755 --- a/platforms/swift/Scripts/xcode_run +++ b/platforms/swift/Scripts/xcode_run @@ -50,12 +50,18 @@ if [[ "$ACTION" == *"test"* && -n "$TEST_FILTER" ]]; then # Find which test target contains this test class (search all test targets automatically) project_root="$(cd "$SCRIPT_DIR/.." && pwd)" test_target="" + target_filter_added="" # Discover all test targets by looking for *Tests directories test_file="" for test_dir in "$project_root/Tests"/*Tests; do if [[ -d "$test_dir" ]]; then target_name=$(basename "$test_dir") + if [[ "$TEST_FILTER" == "$target_name" ]]; then + xcodebuild_cmd="$xcodebuild_cmd -only-testing:$target_name" + target_filter_added="1" + break + fi # Check if TEST_FILTER matches a .swift filename if find "$test_dir" -name "$TEST_FILTER.swift" -type f | grep -q .; then test_target="$target_name" @@ -65,7 +71,9 @@ if [[ "$ACTION" == *"test"* && -n "$TEST_FILTER" ]]; then fi done - if [[ -n "$test_target" && -n "$test_file" ]]; then + if [[ -n "$target_filter_added" ]]; then + : + elif [[ -n "$test_target" && -n "$test_file" ]]; then # Extract all test class names from the file test_classes=$(grep "class.*XCTestCase" "$test_file" | sed 's/.*class \([^:]*\):.*/\1/')