diff --git a/.env.example b/.env.example index 2674c44d4..33060cd54 100644 --- a/.env.example +++ b/.env.example @@ -13,6 +13,10 @@ STOREFRONT_ACCESS_TOKEN=your-public-storefront-access-token # Optional Apple Pay merchant identifier used by accelerated checkout flows. STOREFRONT_MERCHANT_IDENTIFIER= +# Apple team that signs the Swift samples. Set your own team id, or the Xcode build +# fails at signing. A blank value stays blank; setup applies no default team. +DEVELOPMENT_TEAM= + # Storefront API version API_VERSION=2026-04 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75d7232bb..5b360a66f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - 'Package.swift' - 'Package.resolved' - '.github/workflows/swift-test-package.yml' - - '.github/workflows/swift-build-samples.yml' + - '.github/workflows/swift-build-and-test-samples.yml' - '.github/workflows/swift-test-workflow.yml' - '.github/workflows/swift-lint.yml' - '.github/workflows/breaking-changes.yml' @@ -129,11 +129,11 @@ jobs: if: needs.changes.outputs.swift == 'true' uses: ./.github/workflows/swift-test-package.yml - swift-build-samples: + swift-build-and-test-samples: name: Swift needs: changes if: needs.changes.outputs.swift == 'true' - uses: ./.github/workflows/swift-build-samples.yml + uses: ./.github/workflows/swift-build-and-test-samples.yml swift-lint: name: Swift @@ -250,7 +250,7 @@ jobs: - changes - android-test - swift-test-package - - swift-build-samples + - swift-build-and-test-samples - swift-lint - swift-sample-package-paths - rn-test diff --git a/.github/workflows/rn-test.yml b/.github/workflows/rn-test.yml index 9f957b75d..f8848a566 100644 --- a/.github/workflows/rn-test.yml +++ b/.github/workflows/rn-test.yml @@ -36,3 +36,8 @@ jobs: coverage-summary-path: platforms/react-native/coverage/coverage-summary.json title: 'React Native — Coverage Report' create-new-comment: false + + # Runs without --coverage so the coverage report above stays scoped to the + # published module. + - name: Run sample tests + run: pnpm test --testPathPatterns="sample/src" diff --git a/.github/workflows/swift-build-samples.yml b/.github/workflows/swift-build-and-test-samples.yml similarity index 52% rename from .github/workflows/swift-build-samples.yml rename to .github/workflows/swift-build-and-test-samples.yml index 0821942c4..0cc601740 100644 --- a/.github/workflows/swift-build-samples.yml +++ b/.github/workflows/swift-build-and-test-samples.yml @@ -1,4 +1,4 @@ -name: Swift — Build Sample Apps +name: Swift — Build & Test Sample Apps on: workflow_call: @@ -8,9 +8,9 @@ permissions: contents: read jobs: - build-samples: + build-and-test-samples: uses: ./.github/workflows/swift-test-workflow.yml with: - test-path: ./Scripts/build_samples - job-name: Build Sample Apps + test-path: ./Scripts/build_and_test_samples + job-name: Build & Test Sample Apps setup-storefront-env: true diff --git a/.github/workflows/swift-update-linters.yml b/.github/workflows/swift-update-linters.yml index c87e0caec..5238cd2f9 100644 --- a/.github/workflows/swift-update-linters.yml +++ b/.github/workflows/swift-update-linters.yml @@ -135,7 +135,7 @@ jobs: BODY+="1. **Close and reopen this PR** — this fires a new \`pull_request\` event from your user, which triggers all checks.\n" BODY+="2. **Trigger workflows via CLI:**\n" BODY+="\`\`\`bash\n" - BODY+="gh workflow run swift-build-samples.yml --ref auto/update-linters\n" + BODY+="gh workflow run swift-build-and-test-samples.yml --ref auto/update-linters\n" BODY+="gh workflow run swift-lint.yml --ref auto/update-linters\n" BODY+="gh workflow run swift-test-package.yml --ref auto/update-linters\n" BODY+="\`\`\`\n" diff --git a/dev.yml b/dev.yml index ed91bf30e..9cc230a2a 100644 --- a/dev.yml +++ b/dev.yml @@ -384,10 +384,24 @@ commands: ../Scripts/xcode_run build CheckoutKitSwiftDemo test: desc: | - `dev swift test` - Run all tests for the ShopifyCheckoutKit-Package. - `dev swift test ` - Run only the specified test class. + `dev swift test` - Run all tests for the ShopifyCheckoutKit-Package and the sample apps. + `dev swift test ` - Run only the specified package test class. + `dev swift test sample [test_class_name]` - Run only the sample app test targets. syntax: "[test_class_name]" - run: cd platforms/swift && ./Scripts/xcode_run test ShopifyCheckoutKit-Package "$1" + run: | + set -e + cd platforms/swift + if [ -n "${1:-}" ]; then + ./Scripts/xcode_run test ShopifyCheckoutKit-Package "$1" + else + ./Scripts/xcode_run test ShopifyCheckoutKit-Package + ./Scripts/test_samples + fi + subcommands: + sample: + desc: Run the sample app test targets + syntax: "[test_class_name]" + run: cd platforms/swift && ./Scripts/test_samples "${1:-}" api: desc: Validate or update the public Swift API baseline (platforms/swift/api/*.json) run: | diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/e2e/E2ETestIdsTest.kt b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/e2e/E2ETestIdsTest.kt new file mode 100644 index 000000000..a4961f7d9 --- /dev/null +++ b/platforms/android/samples/CheckoutKitAndroidDemo/app/src/test/java/com/shopify/checkoutkit/androiddemo/e2e/E2ETestIdsTest.kt @@ -0,0 +1,11 @@ +package com.shopify.checkoutkit.androiddemo.e2e + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class E2ETestIdsTest { + @Test + fun `app ready marker matches the maestro flows`() { + assertThat(E2ETestIds.APP_READY).isEqualTo("checkout-kit-sample-ready") + } +} diff --git a/platforms/react-native/sample/src/App.tsx b/platforms/react-native/sample/src/App.tsx index f05730fbd..7088db8c7 100644 --- a/platforms/react-native/sample/src/App.tsx +++ b/platforms/react-native/sample/src/App.tsx @@ -50,7 +50,7 @@ import ErrorBoundary from './ErrorBoundary'; import env from 'react-native-config'; import {createDebugLogger} from './utils'; import {useShopifyEventHandlers} from './hooks/useCheckoutEventHandlers'; -import {useE2ECartBootstrap} from './hooks/useE2ECartBootstrap'; +import {useE2ECartBootstrap} from './e2e/useE2ECartBootstrap'; import {E2ETestIds} from './e2e/testIds'; const log = createDebugLogger('ENV'); diff --git a/platforms/react-native/sample/src/linking/__tests__/cartBootstrap.test.ts b/platforms/react-native/sample/src/e2e/__tests__/cartBootstrap.test.ts similarity index 100% rename from platforms/react-native/sample/src/linking/__tests__/cartBootstrap.test.ts rename to platforms/react-native/sample/src/e2e/__tests__/cartBootstrap.test.ts diff --git a/platforms/react-native/sample/src/linking/cartBootstrap.ts b/platforms/react-native/sample/src/e2e/cartBootstrap.ts similarity index 100% rename from platforms/react-native/sample/src/linking/cartBootstrap.ts rename to platforms/react-native/sample/src/e2e/cartBootstrap.ts diff --git a/platforms/react-native/sample/src/hooks/useE2ECartBootstrap.ts b/platforms/react-native/sample/src/e2e/useE2ECartBootstrap.ts similarity index 92% rename from platforms/react-native/sample/src/hooks/useE2ECartBootstrap.ts rename to platforms/react-native/sample/src/e2e/useE2ECartBootstrap.ts index 35132b745..e75eecd8c 100644 --- a/platforms/react-native/sample/src/hooks/useE2ECartBootstrap.ts +++ b/platforms/react-native/sample/src/e2e/useE2ECartBootstrap.ts @@ -1,11 +1,8 @@ import {useCallback} from 'react'; import {Alert} from 'react-native'; import {useCart} from '../context/Cart'; -import { - parseCartBootstrapLink, - type CartBootstrapLink, -} from '../linking/cartBootstrap'; -import useShopify from './useShopify'; +import useShopify from '../hooks/useShopify'; +import {parseCartBootstrapLink, type CartBootstrapLink} from './cartBootstrap'; type UseE2ECartBootstrapOptions = { onCartReady: () => void; diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/E2E/E2ETestIdsTests.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/E2E/E2ETestIdsTests.swift new file mode 100644 index 000000000..4ddeb0562 --- /dev/null +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemoTests/E2E/E2ETestIdsTests.swift @@ -0,0 +1,8 @@ +@testable import CheckoutKitSwiftDemo +import XCTest + +class E2ETestIdsTests: XCTestCase { + func testAppReadyMarkerMatchesTheMaestroFlows() { + XCTAssertEqual(E2ETestIds.appReady, "checkout-kit-sample-ready") + } +} diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/project.yml b/platforms/swift/Samples/CheckoutKitSwiftDemo/project.yml index 8a5e97cb2..54193e7cf 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/project.yml +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/project.yml @@ -19,10 +19,13 @@ schemes: build: targets: CheckoutKitSwiftDemo: all + CheckoutKitSwiftDemoTests: [test] run: config: Debug test: config: Debug + targets: + - CheckoutKitSwiftDemoTests profile: config: Release analyze: @@ -47,7 +50,7 @@ targets: settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.shopify.checkoutkit.swiftdemo - DEVELOPMENT_TEAM: A7XGC83MZE + DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM) SWIFT_VERSION: "6.0" SWIFT_STRICT_CONCURRENCY: complete SWIFT_TREAT_WARNINGS_AS_ERRORS: YES @@ -128,3 +131,30 @@ targets: com.apple.developer.associated-domains: - webcredentials:$(STOREFRONT_DOMAIN)?mode=developer - applinks:$(STOREFRONT_DOMAIN)?mode=developer + + CheckoutKitSwiftDemoTests: + type: bundle.unit-test + platform: iOS + sources: + - path: CheckoutKitSwiftDemoTests + dependencies: + - target: CheckoutKitSwiftDemo + configFiles: + Debug: Storefront.xcconfig + Release: Storefront.xcconfig + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: com.shopify.checkoutkit.swiftdemo.tests + DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM) + SWIFT_VERSION: "6.0" + SWIFT_STRICT_CONCURRENCY: complete + SWIFT_TREAT_WARNINGS_AS_ERRORS: YES + TEST_HOST: $(BUILT_PRODUCTS_DIR)/CheckoutKitSwiftDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CheckoutKitSwiftDemo + BUNDLE_LOADER: $(TEST_HOST) + LD_RUNPATH_SEARCH_PATHS: + - $(inherited) + - "@executable_path/Frameworks" + - "@loader_path/Frameworks" + info: + path: CheckoutKitSwiftDemoTests/Info.plist + properties: {} diff --git a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/project.yml b/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/project.yml index b266522b3..ab203735a 100644 --- a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/project.yml +++ b/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/project.yml @@ -49,7 +49,7 @@ targets: settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.shopify.example.ShopifyAcceleratedCheckoutsApp - DEVELOPMENT_TEAM: A7XGC83MZE + DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM) SWIFT_VERSION: "6.0" TARGETED_DEVICE_FAMILY: "1,2" MARKETING_VERSION: "1.0.1" diff --git a/platforms/swift/Scripts/build_and_test_samples b/platforms/swift/Scripts/build_and_test_samples new file mode 100755 index 000000000..e2d29fcb4 --- /dev/null +++ b/platforms/swift/Scripts/build_and_test_samples @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +SAMPLES_DIR="$SCRIPT_DIR/../Samples" + +if ! "$REPO_ROOT/scripts/setup_storefront_env" --check; then + echo "Run dev up from the repo root to sync sample app storefront configuration." + exit 1 +fi + +"$SCRIPT_DIR/generate_xcode_projects" + +cd "$SAMPLES_DIR" + +run_app() { + local app="$1" + local action="$2" + + if [[ ! -f "$app/Storefront.xcconfig" ]]; then + echo "Missing Storefront.xcconfig for $app. Run dev up from the repo root." + exit 1 + fi + + "$SCRIPT_DIR/xcode_run" "$action" "$app" +} + +run_app CheckoutKitSwiftDemo "clean build test" +run_app ShopifyAcceleratedCheckoutsApp "clean build" diff --git a/platforms/swift/Scripts/test_samples b/platforms/swift/Scripts/test_samples new file mode 100755 index 000000000..8ad917cff --- /dev/null +++ b/platforms/swift/Scripts/test_samples @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +SAMPLES_DIR="$SCRIPT_DIR/../Samples" +TEST_FILTER="${1:-}" + +if ! "$REPO_ROOT/scripts/setup_storefront_env" --check; then + echo "Run dev up from the repo root to sync sample app storefront configuration." + exit 1 +fi + +"$SCRIPT_DIR/generate_xcode_projects" + +if [[ ! -f "$SAMPLES_DIR/CheckoutKitSwiftDemo/Storefront.xcconfig" ]]; then + echo "Missing Storefront.xcconfig for CheckoutKitSwiftDemo. Run dev up from the repo root." + exit 1 +fi + +if [[ -n "$TEST_FILTER" && "$TEST_FILTER" != */* ]]; then + TEST_FILTER="CheckoutKitSwiftDemoTests/$TEST_FILTER" +fi + +cd "$SAMPLES_DIR" + +"$SCRIPT_DIR/xcode_run" test CheckoutKitSwiftDemo "$TEST_FILTER" diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift index 26294508d..272ce8456 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/CheckoutWebViewTests.swift @@ -421,7 +421,7 @@ class CheckoutWebViewTests: XCTestCase { pollForInvalidation() - wait(for: [invalidated], timeout: 2) + wait(for: [invalidated], timeout: 3) } func testInvalidateDetachesCachedPreloadedWebView() { @@ -864,7 +864,7 @@ class CheckoutWebViewTests: XCTestCase { view.userContentController(WKUserContentController(), didReceive: message) - await fulfillment(of: [responseSent], timeout: 1.0) + await fulfillment(of: [responseSent], timeout: 5.0) let response = try XCTUnwrap(MockCheckoutBridge.lastResponseBody) let parsed = try XCTUnwrap(try JSONSerialization.jsonObject(with: Data(response.utf8)) as? [String: Any]) XCTAssertEqual(parsed["id"] as? String, "r") diff --git a/scripts/setup_storefront_env b/scripts/setup_storefront_env index f086f55a6..d0f72088a 100755 --- a/scripts/setup_storefront_env +++ b/scripts/setup_storefront_env @@ -226,6 +226,7 @@ root_has_canonical_keys() { STOREFRONT_DOMAIN \ STOREFRONT_ACCESS_TOKEN \ STOREFRONT_MERCHANT_IDENTIFIER \ + DEVELOPMENT_TEAM \ API_VERSION \ CUSTOMER_ACCOUNT_API_CLIENT_ID \ CUSTOMER_ACCOUNT_API_SHOP_ID \ @@ -454,6 +455,11 @@ load_values() { PROVINCE_VALUE="$(root_or_source_value PROVINCE "$(env_fallback PROVINCE)" "$(read_env_value PROVINCE "$REACT_NATIVE_ENV")" "$(read_env_value PROVINCE "$SWIFT_DEMO_XCCONFIG")" "ON")" ZIP_VALUE="$(root_or_source_value ZIP "$(env_fallback ZIP)" "$(read_env_value ZIP "$REACT_NATIVE_ENV")" "$(read_env_value ZIP "$SWIFT_DEMO_XCCONFIG")" "M5V 1M7")" PHONE_VALUE="$(root_or_source_value PHONE "$(env_fallback PHONE)" "$(read_env_value PHONE "$ANDROID_ENV")" "$(read_env_value PREFILL_PHONE "$ANDROID_ENV")" "$(read_env_value PHONE "$REACT_NATIVE_ENV")" "$(read_env_value PHONE "$SWIFT_DEMO_XCCONFIG")" "+14165550100")" + + # No default and no generated-file fallback: a cleared DEVELOPMENT_TEAM stays cleared, and + # the Xcode build fails at signing until the developer supplies their own team. + DEVELOPMENT_TEAM_VALUE="$(root_or_source_nonempty_value DEVELOPMENT_TEAM \ + "$(env_fallback DEVELOPMENT_TEAM)")" } collect_missing_values() { @@ -507,6 +513,12 @@ EOF write_env_assignment STOREFRONT_MERCHANT_IDENTIFIER "$STOREFRONT_MERCHANT_IDENTIFIER_VALUE" cat <"$output" 2>&1 + + assert_output_is_sanitized "$output" + assert_contains "$demo_xcconfig" "DEVELOPMENT_TEAM =" + assert_not_contains "$demo_xcconfig" "DEVELOPMENT_TEAM = A7XGC83MZE" + # .env has to carry the key as well, or the next normalizing rewrite drops the override. + assert_contains "$fixture/.env" "DEVELOPMENT_TEAM=" + + awk ' + /^DEVELOPMENT_TEAM=/ { print "DEVELOPMENT_TEAM=SYNTHETIC9"; next } + { print } + ' "$fixture/.env" >"$fixture/.env.next" + mv "$fixture/.env.next" "$fixture/.env" + + "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + assert_output_is_sanitized "$output" + assert_contains "$demo_xcconfig" "DEVELOPMENT_TEAM = SYNTHETIC9" + assert_contains "$accelerated_xcconfig" "DEVELOPMENT_TEAM = SYNTHETIC9" + assert_contains "$fixture/.env" "DEVELOPMENT_TEAM=SYNTHETIC9" + + awk ' + /^DEVELOPMENT_TEAM=/ { print "DEVELOPMENT_TEAM="; next } + { print } + ' "$fixture/.env" >"$fixture/.env.next" + mv "$fixture/.env.next" "$fixture/.env" + + "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + assert_output_is_sanitized "$output" + assert_not_contains "$demo_xcconfig" "SYNTHETIC9" + assert_not_contains "$accelerated_xcconfig" "SYNTHETIC9" +} + +# A clone whose .env predates DEVELOPMENT_TEAM holds every other canonical key, so the +# normalizing rewrite has to notice the one absent key. Otherwise dev up leaves .env alone +# and the developer never sees the key they are meant to override. +test_development_team_is_added_to_an_existing_env() { + local fixture output accelerated_xcconfig + fixture="$(make_fixture)" + output="$fixture/output.log" + accelerated_xcconfig="$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" + + STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com \ + STOREFRONT_ACCESS_TOKEN=synthetic-token \ + "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + + grep -v '^DEVELOPMENT_TEAM=' "$fixture/.env" >"$fixture/.env.next" + mv "$fixture/.env.next" "$fixture/.env" + + if "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1; then + fail "root .env without DEVELOPMENT_TEAM passed --check" + fi + assert_output_is_sanitized "$output" + + "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + assert_output_is_sanitized "$output" + assert_contains "$fixture/.env" "DEVELOPMENT_TEAM=" + assert_contains "$accelerated_xcconfig" "DEVELOPMENT_TEAM =" +} + test_migration_from_platform_config() { local fixture output android_env fixture="$(make_fixture)" @@ -277,6 +349,8 @@ test_required_values_only test_optional_sync_updates_blank_optional_values_after_required_setup test_normalizes_existing_root_env test_blank_customer_account_api_version_defaults +test_development_team_follows_env_and_clears +test_development_team_is_added_to_an_existing_env test_migration_from_platform_config echo "setup_storefront_env synthetic tests passed."