diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..00f2b847 --- /dev/null +++ b/.env.example @@ -0,0 +1,33 @@ +# Checkout Kit sample storefront configuration. +# Copy this file to .env, fill in local values, then run: +# scripts/setup_storefront_env +# Optional Apple Pay and Customer Account API values can stay blank. +# +# Do not commit real values from .env or generated platform config files. + +# Storefront details +STOREFRONT_DOMAIN=your-store.myshopify.com +STOREFRONT_ACCESS_TOKEN=your-public-storefront-access-token +# Optional Apple Pay merchant identifier used by accelerated checkout flows. +STOREFRONT_MERCHANT_IDENTIFIER= + +# Storefront API version +API_VERSION=2026-04 + +# Customer Account API (optional) +CUSTOMER_ACCOUNT_API_CLIENT_ID= +CUSTOMER_ACCOUNT_API_SHOP_ID= +CUSTOMER_ACCOUNT_API_VERSION=2026-04 + +# Buyer identity defaults used by sample apps +EMAIL=checkout-kit@example.com +ADDRESS_1=650 King Street +ADDRESS_2=Shopify HQ +CITY=Toronto +COMPANY=Shopify +COUNTRY=CA +FIRST_NAME=Evelyn +LAST_NAME=Hartley +PROVINCE=ON +ZIP=M5V 1M7 +PHONE=+14165550100 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ae63d3ed..8a917e41 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -50,6 +50,11 @@ 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`. +Sample app storefront configuration is generated from the repo-root `.env`. +Shopify employees get this through `dev up`. External contributors can copy +`.env.example` to `.env`, fill in local storefront values, then run +`scripts/setup_storefront_env` from the repo root. + --- ## Swift (`platforms/swift/`) diff --git a/.github/workflows/android-test.yml b/.github/workflows/android-test.yml index 41e89565..01bc389f 100644 --- a/.github/workflows/android-test.yml +++ b/.github/workflows/android-test.yml @@ -47,8 +47,10 @@ jobs: cache: 'gradle' - name: Setup sample app environment - run: cp .env.example .env - working-directory: platforms/android/samples/MobileBuyIntegration + run: ${{ github.workspace }}/scripts/setup_storefront_env --skip-optional-prompts + env: + STOREFRONT_DOMAIN: example.myshopify.com + STOREFRONT_ACCESS_TOKEN: test-token - name: Build Sample App run: ./gradlew assembleDebug diff --git a/.github/workflows/rn-test-android.yml b/.github/workflows/rn-test-android.yml index 6f026b35..c913a02a 100644 --- a/.github/workflows/rn-test-android.yml +++ b/.github/workflows/rn-test-android.yml @@ -58,10 +58,12 @@ jobs: env: GRADLE_OPTS: -Xmx4g -XX:MaxMetaspaceSize=768m JAVA_HOME: ${{ steps.setup-java.outputs.path }} + STOREFRONT_DOMAIN: example.myshopify.com + STOREFRONT_ACCESS_TOKEN: test-token run: | echo "JAVA_HOME: $JAVA_HOME" java -version javac -version - echo "STOREFRONT_DOMAIN=myshopify.com" > sample/.env + ${{ github.workspace }}/scripts/setup_storefront_env --skip-optional-prompts pnpm module build pnpm sample test:android --no-daemon diff --git a/.github/workflows/swift-build-samples.yml b/.github/workflows/swift-build-samples.yml index df8067e0..0821942c 100644 --- a/.github/workflows/swift-build-samples.yml +++ b/.github/workflows/swift-build-samples.yml @@ -13,3 +13,4 @@ jobs: with: test-path: ./Scripts/build_samples job-name: Build Sample Apps + setup-storefront-env: true diff --git a/.github/workflows/swift-test-workflow.yml b/.github/workflows/swift-test-workflow.yml index 14153b36..73210794 100644 --- a/.github/workflows/swift-test-workflow.yml +++ b/.github/workflows/swift-test-workflow.yml @@ -8,6 +8,10 @@ on: required: false type: string default: Run + setup-storefront-env: + required: false + type: boolean + default: false permissions: contents: read @@ -68,5 +72,12 @@ jobs: echo "CURRENT_SIMULATOR_UUID=$CURRENT_SIMULATOR_UUID" >> $GITHUB_ENV echo "" + - name: Setup sample app storefront configuration + if: ${{ inputs.setup-storefront-env }} + env: + STOREFRONT_DOMAIN: example.myshopify.com + STOREFRONT_ACCESS_TOKEN: test-token + run: ${{ github.workspace }}/scripts/setup_storefront_env --skip-optional-prompts + - name: Run Tests run: ${{ inputs.test-path }} diff --git a/platforms/android/samples/MobileBuyIntegration/.env.example b/platforms/android/samples/MobileBuyIntegration/.env.example deleted file mode 100644 index 0e3a417d..00000000 --- a/platforms/android/samples/MobileBuyIntegration/.env.example +++ /dev/null @@ -1,14 +0,0 @@ -STOREFRONT_DOMAIN= -STOREFRONT_ACCESS_TOKEN= -API_VERSION=2026-04 - -CUSTOMER_ACCOUNT_API_CLIENT_ID= -CUSTOMER_ACCOUNT_API_REDIRECT_URI=shop..app://callback - -CUSTOMER_ACCOUNT_API_GRAPHQL_BASE_URL=https://shopify.com//account/customer/api//graphql -CUSTOMER_ACCOUNT_API_AUTH_BASE_URL=https://shopify.com/authentication/ - -# Demo buyer identity used when the "Prefill checkout" toggle is enabled in Settings. -# Phone must be in E.164 format (+1XXXXXXXXXX for Canada). -PREFILL_EMAIL=test.buyer@example.com -PREFILL_PHONE=+16135550123 diff --git a/platforms/android/samples/MobileBuyIntegration/README.md b/platforms/android/samples/MobileBuyIntegration/README.md index ded94669..16edb0ed 100644 --- a/platforms/android/samples/MobileBuyIntegration/README.md +++ b/platforms/android/samples/MobileBuyIntegration/README.md @@ -66,13 +66,14 @@ Do not edit files in `app/build/generated/source/apollo/` by hand. Update `.grap ## Setup -From this directory: +From the repo root: ```sh cp .env.example .env +scripts/setup_storefront_env ``` -Edit `.env`: +Edit the repo-root `.env`: ```text STOREFRONT_DOMAIN=your-store.myshopify.com @@ -84,19 +85,21 @@ Optional values enable Customer Account API and buyer identity demo flows: ```text CUSTOMER_ACCOUNT_API_CLIENT_ID=your-client-id -CUSTOMER_ACCOUNT_API_REDIRECT_URI=shop..app://callback -CUSTOMER_ACCOUNT_API_GRAPHQL_BASE_URL=https://shopify.com//account/customer/api//graphql -CUSTOMER_ACCOUNT_API_AUTH_BASE_URL=https://shopify.com/authentication/ -PREFILL_EMAIL=test.buyer@example.com -PREFILL_PHONE=+16135550123 +CUSTOMER_ACCOUNT_API_SHOP_ID=your-shop-id +CUSTOMER_ACCOUNT_API_VERSION=2026-04 +EMAIL=test.buyer@example.com +PHONE=+16135550123 ``` +The setup script generates this sample's local `.env`. + Open the project in Android Studio, sync Gradle, then build and run. ## Updating the Storefront API version -1. Update `API_VERSION` in `.env`. -2. Download the schema with Rover. This introspects your store's Storefront API and writes `schema.graphqls` into `app/src/main/graphql/`. +1. Update `API_VERSION` in the repo-root `.env`. +2. Run `scripts/setup_storefront_env` from the repo root. +3. Download the schema with Rover. This introspects your store's Storefront API and writes `schema.graphqls` into `app/src/main/graphql/`. ```sh rover graph introspect \ @@ -105,7 +108,7 @@ Open the project in Android Studio, sync Gradle, then build and run. --output "app/src/main/graphql/schema.graphqls" ``` -3. Update GraphQL operations in `app/src/main/graphql/` if the schema changed. For example, add a product field to `FetchProducts.graphql` before regenerating types: +4. Update GraphQL operations in `app/src/main/graphql/` if the schema changed. For example, add a product field to `FetchProducts.graphql` before regenerating types: ```graphql query FetchProducts(...) { diff --git a/platforms/android/samples/MobileBuyIntegration/app/build.gradle b/platforms/android/samples/MobileBuyIntegration/app/build.gradle index 24679cb9..4427db88 100644 --- a/platforms/android/samples/MobileBuyIntegration/app/build.gradle +++ b/platforms/android/samples/MobileBuyIntegration/app/build.gradle @@ -20,6 +20,11 @@ def loadProperties() { def properties = loadProperties() +def propertyOrDefault = { String key, String defaultValue -> + def value = properties.getProperty(key) + return value?.trim() ? value.trim() : defaultValue +} + // Storefront API def storefrontDomain = properties.getProperty("STOREFRONT_DOMAIN") def accessToken = properties.getProperty("STOREFRONT_ACCESS_TOKEN") @@ -27,14 +32,28 @@ def apiVersion = properties.getProperty("API_VERSION", "2026-04") // Customer Account API def customerAccountApiClientId = properties.getProperty("CUSTOMER_ACCOUNT_API_CLIENT_ID") +def customerAccountApiShopId = properties.getProperty("CUSTOMER_ACCOUNT_API_SHOP_ID") +def customerAccountApiVersion = propertyOrDefault("CUSTOMER_ACCOUNT_API_VERSION", "unstable") def customerAccountApiRedirectUri = properties.getProperty("CUSTOMER_ACCOUNT_API_REDIRECT_URI") def customerAccountApiGraphQLBaseUrl = properties.getProperty("CUSTOMER_ACCOUNT_API_GRAPHQL_BASE_URL") def customerAccountApiAuthBaseUrl = properties.getProperty("CUSTOMER_ACCOUNT_API_AUTH_BASE_URL") +if (!customerAccountApiRedirectUri && customerAccountApiShopId) { + customerAccountApiRedirectUri = "shop.${customerAccountApiShopId}.app://callback" +} + +if (!customerAccountApiGraphQLBaseUrl && customerAccountApiShopId) { + customerAccountApiGraphQLBaseUrl = "https://shopify.com/${customerAccountApiShopId}/account/customer/api/${customerAccountApiVersion}/graphql" +} + +if (!customerAccountApiAuthBaseUrl && customerAccountApiShopId) { + customerAccountApiAuthBaseUrl = "https://shopify.com/authentication/${customerAccountApiShopId}" +} + // Demo buyer identity (prefill toggle in Settings) -def prefillEmail = properties.getProperty("PREFILL_EMAIL", "test.buyer@example.com") -def prefillPhone = properties.getProperty("PREFILL_PHONE", "+16135550123") +def prefillEmail = properties.getProperty("EMAIL", properties.getProperty("PREFILL_EMAIL", "test.buyer@example.com")) +def prefillPhone = properties.getProperty("PHONE", properties.getProperty("PREFILL_PHONE", "+14165550100")) if (!storefrontDomain || !accessToken) { println("**** Please add a .env file with STOREFRONT_DOMAIN and STOREFRONT_ACCESS_TOKEN set *****") diff --git a/platforms/android/samples/README.md b/platforms/android/samples/README.md index 42f62677..badf78c8 100644 --- a/platforms/android/samples/README.md +++ b/platforms/android/samples/README.md @@ -8,13 +8,14 @@ This directory contains Android sample apps for Checkout Kit. ## Setup -From `platforms/android/samples/MobileBuyIntegration`: +From the repo root: ```sh cp .env.example .env +scripts/setup_storefront_env ``` -Fill in: +Fill `.env` with: - `STOREFRONT_DOMAIN` - `STOREFRONT_ACCESS_TOKEN` @@ -22,4 +23,6 @@ Fill in: - Optional Customer Account API values - Optional demo buyer identity values +The setup script generates `platforms/android/samples/MobileBuyIntegration/.env`. + Open `MobileBuyIntegration` in Android Studio, sync Gradle, then build and run the `app` target. diff --git a/platforms/android/scripts/apollo_download_schema b/platforms/android/scripts/apollo_download_schema index d8ab6283..28b61809 100755 --- a/platforms/android/scripts/apollo_download_schema +++ b/platforms/android/scripts/apollo_download_schema @@ -3,44 +3,66 @@ set -euo pipefail cd samples/MobileBuyIntegration -if [ ! -f ".env" ]; then +ENV_FILE=".env" + +if [ ! -f "$ENV_FILE" ]; then echo "❌ .env file not found in samples/MobileBuyIntegration/" - echo " Run 'cp .env.example .env' and fill in your credentials." + echo " Run scripts/setup_storefront_env from the repo root." exit 1 fi -npx -y dotenv-cli -e .env -- sh -c ' - DOMAIN="$STOREFRONT_DOMAIN" - TOKEN="$STOREFRONT_ACCESS_TOKEN" - VERSION="$API_VERSION" - - if [ -z "$DOMAIN" ]; then - echo "❌ STOREFRONT_DOMAIN is not set. Check your .env file." - exit 1 - fi - if [ -z "$TOKEN" ]; then - echo "❌ STOREFRONT_ACCESS_TOKEN is not set. Check your .env file." - exit 1 - fi - if [ -z "$VERSION" ]; then - echo "❌ API_VERSION is not set. Check your .env file." - echo " Add API_VERSION=2026-04 to your .env" - exit 1 - fi - - echo "📡 Downloading schema for MobileBuyIntegration..." - echo " Domain: $DOMAIN" - echo " API Version: $VERSION" - - rover graph introspect \ - "https://$DOMAIN/api/$VERSION/graphql" \ - --header="X-Shopify-Storefront-Access-Token: $TOKEN" \ - --output "app/src/main/graphql/schema.graphqls" - - if [ $? -eq 0 ]; then - echo "✅ Schema downloaded to app/src/main/graphql/schema.graphqls" - else - echo "❌ Schema download failed. Check your network connection and credentials." - exit 1 - fi -' +read_env_value() { + local key="$1" + awk -v key="$key" ' + /^[[:space:]]*#/ || /^[[:space:]]*\/\// || /^[[:space:]]*$/ { next } + $0 !~ /=/ { next } + { + line = $0 + sub(/^[[:space:]]*/, "", line) + candidate = line + sub(/=.*/, "", candidate) + sub(/[[:space:]]*$/, "", candidate) + if (candidate == key) { + value = substr(line, index(line, "=") + 1) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + if (value ~ /^".*"$/ || value ~ /^'\''.*'\''$/) { + value = substr(value, 2, length(value) - 2) + } + print value + exit + } + } + ' "$ENV_FILE" +} + +DOMAIN="$(read_env_value STOREFRONT_DOMAIN)" +TOKEN="$(read_env_value STOREFRONT_ACCESS_TOKEN)" +VERSION="$(read_env_value API_VERSION)" + +if [ -z "$DOMAIN" ]; then + echo "❌ STOREFRONT_DOMAIN is not set. Check your .env file." + exit 1 +fi +if [ -z "$TOKEN" ]; then + echo "❌ STOREFRONT_ACCESS_TOKEN is not set. Check your .env file." + exit 1 +fi +if [ -z "$VERSION" ]; then + echo "❌ API_VERSION is not set. Check your .env file." + echo " Add API_VERSION=2026-04 to your .env" + exit 1 +fi + +echo "📡 Downloading schema for MobileBuyIntegration..." +echo " Domain: $DOMAIN" +echo " API Version: $VERSION" + +if rover graph introspect \ + "https://$DOMAIN/api/$VERSION/graphql" \ + --header="X-Shopify-Storefront-Access-Token: $TOKEN" \ + --output "app/src/main/graphql/schema.graphqls"; then + echo "✅ Schema downloaded to app/src/main/graphql/schema.graphqls" +else + echo "❌ Schema download failed. Check your network connection and credentials." + exit 1 +fi diff --git a/platforms/android/scripts/setup_env.sh b/platforms/android/scripts/setup_env.sh index 013a5a2e..952b0a04 100755 --- a/platforms/android/scripts/setup_env.sh +++ b/platforms/android/scripts/setup_env.sh @@ -1,57 +1,7 @@ #!/bin/bash -# Function to create .env file for a sample app -create_env_file() { - local app_name="$1" - local env_path="./samples/${app_name}/.env" +set -e - if [ ! -f "$env_path" ]; then - echo "Creating ${app_name} .env file..." - cat >"$env_path" <>"$env_path" <(); export const cache = new InMemoryCache(); const client = new ApolloClient({ - uri: `https://${env.STOREFRONT_DOMAIN}/api/${env.STOREFRONT_VERSION}/graphql.json`, + uri: `https://${env.STOREFRONT_DOMAIN}/api/${storefrontApiVersion}/graphql.json`, cache, headers: { 'Content-Type': 'application/json', diff --git a/platforms/swift/Samples/MobileBuyIntegration/README.md b/platforms/swift/Samples/MobileBuyIntegration/README.md index 4f5c3de0..3b95a5b1 100644 --- a/platforms/swift/Samples/MobileBuyIntegration/README.md +++ b/platforms/swift/Samples/MobileBuyIntegration/README.md @@ -63,36 +63,39 @@ Do not edit files in `Generated/` by hand. Update `.graphql` files and regenerat ## Setup -From `platforms/swift`: +From the repo root: ```sh -cp Samples/MobileBuyIntegration/Storefront.xcconfig.example \ - Samples/MobileBuyIntegration/Storefront.xcconfig +cp .env.example .env +scripts/setup_storefront_env ``` -Edit `Storefront.xcconfig`: +Edit `.env`: ```text -STOREFRONT_DOMAIN = your-store.myshopify.com -STOREFRONT_ACCESS_TOKEN = your-token -API_VERSION = 2026-04 +STOREFRONT_DOMAIN=your-store.myshopify.com +STOREFRONT_ACCESS_TOKEN=your-token +API_VERSION=2026-04 ``` Optional values enable Customer Account API and buyer identity demo flows: ```text -CUSTOMER_ACCOUNT_API_CLIENT_ID = your-client-id -CUSTOMER_ACCOUNT_API_SHOP_ID = your-shop-id -EMAIL = test.buyer@example.com -PHONE = +16135550123 +CUSTOMER_ACCOUNT_API_CLIENT_ID=your-client-id +CUSTOMER_ACCOUNT_API_SHOP_ID=your-shop-id +EMAIL=test.buyer@example.com +PHONE=+16135550123 ``` +The setup script generates this sample's `Storefront.xcconfig`. + Open the project in Xcode, let Swift Package Manager resolve dependencies, then build and run. ## Updating the Storefront API version -1. Update `API_VERSION` in `Storefront.xcconfig`. -2. Download the schema with Rover. This introspects your store's Storefront API and writes `schema..graphqls` into the sample app directory. +1. Update `API_VERSION` in the repo-root `.env`. +2. Run `scripts/setup_storefront_env` from the repo root. +3. Download the schema with Rover. This introspects your store's Storefront API and writes `schema..graphqls` into the sample app directory. ```sh rover graph introspect \ @@ -101,7 +104,7 @@ Open the project in Xcode, let Swift Package Manager resolve dependencies, then --output "schema.$API_VERSION.graphqls" ``` -3. Update `.graphql` operations if the schema changed. For example, add a product field to `MobileBuyIntegration/Sources/Api/Queries/GetProducts.graphql` before regenerating types: +4. Update `.graphql` operations if the schema changed. For example, add a product field to `MobileBuyIntegration/Sources/Api/Queries/GetProducts.graphql` before regenerating types: ```graphql query GetProducts(...) { diff --git a/platforms/swift/Samples/MobileBuyIntegration/Scripts/generate_entitlements.sh b/platforms/swift/Samples/MobileBuyIntegration/Scripts/generate_entitlements.sh index 8a284e34..067f8901 100755 --- a/platforms/swift/Samples/MobileBuyIntegration/Scripts/generate_entitlements.sh +++ b/platforms/swift/Samples/MobileBuyIntegration/Scripts/generate_entitlements.sh @@ -20,4 +20,4 @@ fi sed "s/{STOREFRONT_DOMAIN}/$STOREFRONT_DOMAIN?mode=developer/g" "$TEMPLATE_FILE" > "$OUTPUT_FILE" -echo "Success: Entitlements file generated at $OUTPUT_FILE with domain $STOREFRONT_DOMAIN" +echo "Success: Entitlements file generated at $OUTPUT_FILE" diff --git a/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig.example b/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig.example deleted file mode 100644 index fcb81ae6..00000000 --- a/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig.example +++ /dev/null @@ -1,29 +0,0 @@ -// --- Storefront -// --- Find your Storefront API access token under: -// ----- https://admin.shopify.com/store/{STOREFRONT}/settings/apps/development/{APP_ID}/api_credentials - -STOREFRONT_DOMAIN = -STOREFRONT_ACCESS_TOKEN = -STOREFRONT_MERCHANT_IDENTIFIER = - -// --- Customer Account API (optional) -// --- Find these under your Headless app configuration: -// ----- https://admin.shopify.com/store/{STOREFRONT}/settings/apps/development/{APP_ID} -CUSTOMER_ACCOUNT_API_CLIENT_ID = -CUSTOMER_ACCOUNT_API_SHOP_ID = - -// --- Buyer preference data -EMAIL = test-checkout-sdk@shopify.com - -FIRST_NAME = Test -LAST_NAME = Test - -ADDRESS_1 = The Cloak & Dagger -ADDRESS_2 = 1st Street Southeast -CITY = Calgary -COUNTRY = CA -PROVINCE = AB -ZIP = T1X 0L3 -PHONE = +155565708743 - -API_VERSION = 2026-04 diff --git a/platforms/swift/Samples/README.md b/platforms/swift/Samples/README.md index bf27c831..1b3c98c9 100644 --- a/platforms/swift/Samples/README.md +++ b/platforms/swift/Samples/README.md @@ -16,14 +16,14 @@ This directory contains iOS sample apps for Checkout Kit. ## MobileBuyIntegration -From `platforms/swift`: +From the repo root: ```sh -cp Samples/MobileBuyIntegration/Storefront.xcconfig.example \ - Samples/MobileBuyIntegration/Storefront.xcconfig +cp .env.example .env +scripts/setup_storefront_env ``` -Fill in: +Fill `.env` with: - `STOREFRONT_DOMAIN` - `STOREFRONT_ACCESS_TOKEN` @@ -31,26 +31,30 @@ Fill in: - Optional Customer Account API values - Optional demo buyer identity values -Open `Samples/Samples.xcworkspace` or `Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj` in Xcode, then build and run the `MobileBuyIntegration` scheme. +The setup script generates `platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig`. + +Open `platforms/swift/Samples/Samples.xcworkspace` or `platforms/swift/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj` in Xcode, then build and run the `MobileBuyIntegration` scheme. The project generates associated-domain entitlements from `Storefront.xcconfig` during the Xcode build pre-action. ## ShopifyAcceleratedCheckoutsApp -From `platforms/swift`: +From the repo root: ```sh -cp Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig.example \ - Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig +cp .env.example .env +scripts/setup_storefront_env ``` -Fill in: +Fill `.env` with: - `STOREFRONT_DOMAIN` - `STOREFRONT_ACCESS_TOKEN` - `API_VERSION` -Open `Samples/Samples.xcworkspace` or `Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp.xcodeproj` in Xcode, then build and run the `ShopifyAcceleratedCheckoutsApp` scheme. +The setup script generates `platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig`. + +Open `platforms/swift/Samples/Samples.xcworkspace` or `platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/ShopifyAcceleratedCheckoutsApp.xcodeproj` in Xcode, then build and run the `ShopifyAcceleratedCheckoutsApp` scheme. ## Troubleshooting @@ -58,6 +62,6 @@ If the build pre-action fails, Xcode usually shows `exited with status code 1`. | Build log output | Cause | Fix | | --- | --- | --- | -| `grep: Storefront.xcconfig: No such file or directory` | The sample config file is missing. | Copy `.xcconfig.example` to `Storefront.xcconfig`. | +| `grep: Storefront.xcconfig: No such file or directory` | The sample config file is missing. | Run `scripts/setup_storefront_env` from the repo root. | | `Error: STOREFRONT_DOMAIN is not set in Storefront.xcconfig` | `STOREFRONT_DOMAIN` is blank. | Set your shop domain without `https://`. | | Associated domains do not work at runtime | Domain or app association is wrong. | Verify your custom storefront domain, app ID, and Universal Links setup. | diff --git a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/.gitignore b/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/.gitignore deleted file mode 100644 index 299cebcd..00000000 --- a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!Storefront.xcconfig.example \ No newline at end of file diff --git a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig.example b/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig.example deleted file mode 100644 index aea5720c..00000000 --- a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig.example +++ /dev/null @@ -1,10 +0,0 @@ -// --- Storefront -// --- Find your Storefront API access token under: -// ----- https://admin.shopify.com/store/{STOREFRONT}/settings/apps/development/{APP_ID}/api_credentials - -// Configuration settings file format documentation can be found at: -// https://help.apple.com/xcode/#/dev745c5c974 - -STOREFRONT_DOMAIN = shopify.my-shopify.com -STOREFRONT_ACCESS_TOKEN = 123456 -API_VERSION = 2026-04 diff --git a/platforms/swift/Scripts/build_samples b/platforms/swift/Scripts/build_samples index 499ca62f..b39040e2 100755 --- a/platforms/swift/Scripts/build_samples +++ b/platforms/swift/Scripts/build_samples @@ -1,11 +1,17 @@ - #!/usr/bin/env bash +#!/usr/bin/env bash -set -ex -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +SAMPLES_DIR="$SCRIPT_DIR/../Samples" -cd 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 + +cd "$SAMPLES_DIR" EMPTY_ENTITLEMENTS=""" @@ -17,17 +23,18 @@ EMPTY_ENTITLEMENTS=""" build_app() { if [[ ! -f "$1/Storefront.xcconfig" ]]; then - cp "$1/Storefront.xcconfig.example" "$1/Storefront.xcconfig" + echo "Missing Storefront.xcconfig for $1. Run dev up from the repo root." + exit 1 fi # Create an empty entitlements file if it doesn't already exist if [[ ! -f "$1/$1/$1.entitlements" ]]; then - echo $EMPTY_ENTITLEMENTS > "$1/$1/$1.entitlements" + printf '%s\n' "$EMPTY_ENTITLEMENTS" > "$1/$1/$1.entitlements" else echo "Entitlements file already exists for $1 project." fi - $SCRIPT_DIR/xcode_run "clean build" $1 + "$SCRIPT_DIR/xcode_run" "clean build" "$1" } build_app MobileBuyIntegration diff --git a/platforms/swift/Scripts/ensure_storefront_config b/platforms/swift/Scripts/ensure_storefront_config index e7dc924b..c87a9047 100755 --- a/platforms/swift/Scripts/ensure_storefront_config +++ b/platforms/swift/Scripts/ensure_storefront_config @@ -1,14 +1,7 @@ - #!/usr/bin/env bash +#!/usr/bin/env bash set -e -if [ ! -f "./samples/MobileBuyIntegration/Storefront.xcconfig" ]; then - echo """ - Error: Your project is missing a Storefront.xcconfig file. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - Replace the STOREFRONT_DOMAIN and STOREFRONT_ACCESS_TOKEN environment variables in \"samples/MobileBuyIntegration/Storefront.xcconfig.example\" and rename the file to \"Storefront.xcconfig\" to get started. - - If you don't have a Shopify app setup, go to https://admin.shopify.com/settings/apps/development to configure an application for your storefront which will give you access to the Storefront API. - """ - exit 1; -fi +exec "$SCRIPT_DIR/../../../scripts/setup_storefront_env" "$@" diff --git a/scripts/setup_storefront_env b/scripts/setup_storefront_env new file mode 100755 index 00000000..77d5c939 --- /dev/null +++ b/scripts/setup_storefront_env @@ -0,0 +1,749 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +ROOT_ENV="${ROOT_DIR}/.env" +ANDROID_ENV="${ROOT_DIR}/platforms/android/samples/MobileBuyIntegration/.env" +SWIFT_MOBILE_XCCONFIG="${ROOT_DIR}/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig" +SWIFT_ACCELERATED_XCCONFIG="${ROOT_DIR}/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" +REACT_NATIVE_ENV="${ROOT_DIR}/platforms/react-native/sample/.env" + +DEFAULT_API_VERSION="2026-04" +DEFAULT_CUSTOMER_ACCOUNT_API_VERSION="2026-04" + +usage() { + cat <&2 + exit 1 + ;; + esac + shift +done + +trim() { + sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' +} + +strip_outer_quotes() { + local value + value="$(printf '%s' "$1" | trim)" + + case "$value" in + \"*\") + value="${value#\"}" + value="${value%\"}" + ;; + \'*\') + value="${value#\'}" + value="${value%\'}" + ;; + esac + + printf '%s' "$value" +} + +read_env_value() { + local key="$1" + local file="$2" + local raw_value + + if [[ ! -f "$file" ]]; then + return 0 + fi + + raw_value="$(awk -v key="$key" ' + /^[[:space:]]*#/ || /^[[:space:]]*\/\// || /^[[:space:]]*$/ { next } + $0 !~ /=/ { next } + { + line = $0 + sub(/^[[:space:]]*/, "", line) + candidate = line + sub(/=.*/, "", candidate) + sub(/[[:space:]]*$/, "", candidate) + if (candidate == key) { + value = substr(line, index(line, "=") + 1) + print value + exit + } + } + ' "$file")" + + strip_outer_quotes "$raw_value" +} + +env_has_key() { + local key="$1" + local file="$2" + + [[ -f "$file" ]] || return 1 + + awk -v key="$key" ' + /^[[:space:]]*#/ || /^[[:space:]]*\/\// || /^[[:space:]]*$/ { next } + $0 !~ /=/ { next } + { + line = $0 + sub(/^[[:space:]]*/, "", line) + candidate = line + sub(/=.*/, "", candidate) + sub(/[[:space:]]*$/, "", candidate) + if (candidate == key) { + found = 1 + exit + } + } + END { exit found ? 0 : 1 } + ' "$file" +} + +is_missing_required_value() { + local value="$1" + + [[ -z "$value" ]] || + [[ "$value" == \<*\> ]] || + [[ "$value" == "YOUR_"* ]] || + [[ "$value" == "your-store.myshopify.com" ]] || + [[ "$value" == "your-public-storefront-access-token" ]] +} + +is_placeholder_value() { + local value="$1" + + [[ "$value" == \<*\> ]] || + [[ "$value" == "YOUR_"* ]] || + [[ "$value" == "INSERT_"* ]] || + [[ "$value" == *"INSERT_"* ]] || + [[ "$value" == "your-store.myshopify.com" ]] || + [[ "$value" == "your-public-storefront-access-token" ]] +} + +env_fallback() { + local key="$1" + printf '%s' "${!key:-}" +} + +first_config_value() { + local value + for value in "$@"; do + if [[ -n "$value" ]] && ! is_placeholder_value "$value"; then + printf '%s' "$value" + return 0 + fi + done +} + +required_config_value() { + local key="$1" + local file_value + local env_value + + file_value="$(read_env_value "$key" "$ROOT_ENV")" + if ! is_missing_required_value "$file_value"; then + printf '%s' "$file_value" + return 0 + fi + + env_value="$(env_fallback "$key")" + if ! is_missing_required_value "$env_value"; then + printf '%s' "$env_value" + fi +} + +root_or_source_value() { + local key="$1" + shift + local root_value + + if env_has_key "$key" "$ROOT_ENV"; then + root_value="$(read_env_value "$key" "$ROOT_ENV")" + if ! is_placeholder_value "$root_value"; then + printf '%s' "$root_value" + return 0 + fi + fi + + first_config_value "$@" +} + +root_or_source_nonempty_value() { + local key="$1" + shift + local root_value + + if env_has_key "$key" "$ROOT_ENV"; then + root_value="$(read_env_value "$key" "$ROOT_ENV")" + if [[ -n "$root_value" ]] && ! is_placeholder_value "$root_value"; then + printf '%s' "$root_value" + return 0 + fi + fi + + first_config_value "$@" +} + +root_has_canonical_keys() { + local key + + for key in \ + STOREFRONT_DOMAIN \ + STOREFRONT_ACCESS_TOKEN \ + STOREFRONT_MERCHANT_IDENTIFIER \ + API_VERSION \ + CUSTOMER_ACCOUNT_API_CLIENT_ID \ + CUSTOMER_ACCOUNT_API_SHOP_ID \ + CUSTOMER_ACCOUNT_API_VERSION \ + EMAIL \ + ADDRESS_1 \ + ADDRESS_2 \ + CITY \ + COMPANY \ + COUNTRY \ + FIRST_NAME \ + LAST_NAME \ + PROVINCE \ + ZIP \ + PHONE; do + env_has_key "$key" "$ROOT_ENV" || return 1 + done +} + +customer_account_api_version_needs_default() { + local value + + value="$(read_env_value CUSTOMER_ACCOUNT_API_VERSION "$ROOT_ENV")" + [[ -z "$value" ]] || is_placeholder_value "$value" +} + +optional_values_need_prompt() { + [[ "$prompt_optional_values" == "true" ]] && + [[ -t 0 ]] && + ( + [[ -z "$STOREFRONT_MERCHANT_IDENTIFIER_VALUE" ]] || + [[ -z "$CUSTOMER_ACCOUNT_API_CLIENT_ID_VALUE" ]] || + [[ -z "$CUSTOMER_ACCOUNT_API_SHOP_ID_VALUE" ]] + ) +} + +fill_optional_values_from_environment() { + local value + local updated="false" + + if [[ -z "$STOREFRONT_MERCHANT_IDENTIFIER_VALUE" ]]; then + value="$(env_fallback STOREFRONT_MERCHANT_IDENTIFIER)" + if [[ -n "$value" ]] && ! is_placeholder_value "$value"; then + STOREFRONT_MERCHANT_IDENTIFIER_VALUE="$value" + updated="true" + fi + fi + + if [[ -z "$CUSTOMER_ACCOUNT_API_CLIENT_ID_VALUE" ]]; then + value="$(env_fallback CUSTOMER_ACCOUNT_API_CLIENT_ID)" + if [[ -n "$value" ]] && ! is_placeholder_value "$value"; then + CUSTOMER_ACCOUNT_API_CLIENT_ID_VALUE="$value" + updated="true" + fi + fi + + if [[ -z "$CUSTOMER_ACCOUNT_API_SHOP_ID_VALUE" ]]; then + value="$(env_fallback CUSTOMER_ACCOUNT_API_SHOP_ID)" + if [[ -n "$value" ]] && ! is_placeholder_value "$value"; then + CUSTOMER_ACCOUNT_API_SHOP_ID_VALUE="$value" + updated="true" + fi + fi + + [[ "$updated" == "true" ]] +} + +derive_customer_account_api_shop_id() { + local value + + value="$(read_env_value CUSTOMER_ACCOUNT_API_SHOP_ID "$ANDROID_ENV")" + if [[ -n "$value" ]] && ! is_placeholder_value "$value"; then + printf '%s' "$value" + return 0 + fi + + value="$(read_env_value CUSTOMER_ACCOUNT_API_REDIRECT_URI "$ANDROID_ENV")" + case "$value" in + shop.*.app://callback) + value="${value#shop.}" + value="${value%.app://callback}" + if [[ -n "$value" ]] && ! is_placeholder_value "$value"; then + printf '%s' "$value" + return 0 + fi + ;; + esac + + value="$(read_env_value CUSTOMER_ACCOUNT_API_AUTH_BASE_URL "$ANDROID_ENV")" + case "$value" in + https://shopify.com/authentication/*) + value="${value#https://shopify.com/authentication/}" + if [[ -n "$value" ]] && ! is_placeholder_value "$value"; then + printf '%s' "$value" + return 0 + fi + ;; + esac + + value="$(read_env_value CUSTOMER_ACCOUNT_API_GRAPHQL_BASE_URL "$ANDROID_ENV")" + case "$value" in + https://shopify.com/*/account/customer/api/*/graphql) + value="${value#https://shopify.com/}" + value="${value%%/account/customer/api/*}" + if [[ -n "$value" ]] && ! is_placeholder_value "$value"; then + printf '%s' "$value" + fi + ;; + esac +} + +derive_customer_account_api_version() { + local value + + value="$(read_env_value CUSTOMER_ACCOUNT_API_VERSION "$ANDROID_ENV")" + if [[ -n "$value" ]] && ! is_placeholder_value "$value"; then + printf '%s' "$value" + return 0 + fi + + value="$(read_env_value CUSTOMER_ACCOUNT_API_GRAPHQL_BASE_URL "$ANDROID_ENV")" + case "$value" in + https://shopify.com/*/account/customer/api/*/graphql) + value="${value#*/account/customer/api/}" + value="${value%/graphql}" + if [[ -n "$value" ]] && ! is_placeholder_value "$value"; then + printf '%s' "$value" + fi + ;; + esac +} + +prompt_required() { + local label="$1" + local value="" + + if [[ ! -t 0 ]]; then + echo "Storefront configuration requires interactive setup. Run scripts/setup_storefront_env in a terminal." >&2 + exit 1 + fi + + while [[ -z "$value" ]]; do + read -r -s -p "${label}: " value + echo + value="$(printf '%s' "$value" | trim)" + done + + printf '%s' "$value" +} + +prompt_optional() { + local label="$1" + local value="" + + if [[ ! -t 0 ]]; then + return 0 + fi + + read -r -s -p "${label} (optional, press Enter to skip): " value + echo + printf '%s' "$value" | trim +} + +load_values() { + STOREFRONT_DOMAIN_VALUE="$(first_config_value \ + "$(required_config_value STOREFRONT_DOMAIN)" \ + "$(read_env_value STOREFRONT_DOMAIN "$ANDROID_ENV")" \ + "$(read_env_value STOREFRONT_DOMAIN "$REACT_NATIVE_ENV")" \ + "$(read_env_value STOREFRONT_DOMAIN "$SWIFT_MOBILE_XCCONFIG")" \ + "$(read_env_value STOREFRONT_DOMAIN "$SWIFT_ACCELERATED_XCCONFIG")")" + + STOREFRONT_ACCESS_TOKEN_VALUE="$(first_config_value \ + "$(required_config_value STOREFRONT_ACCESS_TOKEN)" \ + "$(read_env_value STOREFRONT_ACCESS_TOKEN "$ANDROID_ENV")" \ + "$(read_env_value STOREFRONT_ACCESS_TOKEN "$REACT_NATIVE_ENV")" \ + "$(read_env_value STOREFRONT_ACCESS_TOKEN "$SWIFT_MOBILE_XCCONFIG")" \ + "$(read_env_value STOREFRONT_ACCESS_TOKEN "$SWIFT_ACCELERATED_XCCONFIG")")" + + API_VERSION_VALUE="$(first_config_value \ + "$(read_env_value API_VERSION "$ROOT_ENV")" \ + "$(read_env_value STOREFRONT_VERSION "$ROOT_ENV")" \ + "$(env_fallback API_VERSION)" \ + "$(env_fallback STOREFRONT_VERSION)" \ + "$(read_env_value API_VERSION "$ANDROID_ENV")" \ + "$(read_env_value API_VERSION "$REACT_NATIVE_ENV")" \ + "$(read_env_value STOREFRONT_VERSION "$REACT_NATIVE_ENV")" \ + "$(read_env_value API_VERSION "$SWIFT_MOBILE_XCCONFIG")" \ + "$(read_env_value API_VERSION "$SWIFT_ACCELERATED_XCCONFIG")" \ + "$DEFAULT_API_VERSION")" + + STOREFRONT_MERCHANT_IDENTIFIER_VALUE="$(root_or_source_value STOREFRONT_MERCHANT_IDENTIFIER \ + "$(env_fallback STOREFRONT_MERCHANT_IDENTIFIER)" \ + "$(read_env_value STOREFRONT_MERCHANT_IDENTIFIER "$ANDROID_ENV")" \ + "$(read_env_value STOREFRONT_MERCHANT_IDENTIFIER "$REACT_NATIVE_ENV")" \ + "$(read_env_value STOREFRONT_MERCHANT_IDENTIFIER "$SWIFT_MOBILE_XCCONFIG")")" + + CUSTOMER_ACCOUNT_API_CLIENT_ID_VALUE="$(root_or_source_value CUSTOMER_ACCOUNT_API_CLIENT_ID \ + "$(env_fallback CUSTOMER_ACCOUNT_API_CLIENT_ID)" \ + "$(read_env_value CUSTOMER_ACCOUNT_API_CLIENT_ID "$ANDROID_ENV")" \ + "$(read_env_value CUSTOMER_ACCOUNT_API_CLIENT_ID "$REACT_NATIVE_ENV")" \ + "$(read_env_value CUSTOMER_ACCOUNT_API_CLIENT_ID "$SWIFT_MOBILE_XCCONFIG")")" + + CUSTOMER_ACCOUNT_API_SHOP_ID_VALUE="$(root_or_source_value CUSTOMER_ACCOUNT_API_SHOP_ID \ + "$(env_fallback CUSTOMER_ACCOUNT_API_SHOP_ID)" \ + "$(derive_customer_account_api_shop_id)" \ + "$(read_env_value CUSTOMER_ACCOUNT_API_SHOP_ID "$REACT_NATIVE_ENV")" \ + "$(read_env_value CUSTOMER_ACCOUNT_API_SHOP_ID "$SWIFT_MOBILE_XCCONFIG")")" + + CUSTOMER_ACCOUNT_API_VERSION_VALUE="$(root_or_source_nonempty_value CUSTOMER_ACCOUNT_API_VERSION \ + "$(env_fallback CUSTOMER_ACCOUNT_API_VERSION)" \ + "$(derive_customer_account_api_version)" \ + "$(read_env_value CUSTOMER_ACCOUNT_API_VERSION "$REACT_NATIVE_ENV")" \ + "$(read_env_value CUSTOMER_ACCOUNT_API_VERSION "$SWIFT_MOBILE_XCCONFIG")" \ + "$(read_env_value CUSTOMER_ACCOUNT_API_VERSION "$SWIFT_ACCELERATED_XCCONFIG")" \ + "$DEFAULT_CUSTOMER_ACCOUNT_API_VERSION")" + + EMAIL_VALUE="$(root_or_source_value EMAIL "$(env_fallback EMAIL)" "$(read_env_value EMAIL "$ANDROID_ENV")" "$(read_env_value PREFILL_EMAIL "$ANDROID_ENV")" "$(read_env_value EMAIL "$REACT_NATIVE_ENV")" "$(read_env_value EMAIL "$SWIFT_MOBILE_XCCONFIG")" "checkout-kit@example.com")" + ADDRESS_1_VALUE="$(root_or_source_value ADDRESS_1 "$(env_fallback ADDRESS_1)" "$(read_env_value ADDRESS_1 "$REACT_NATIVE_ENV")" "$(read_env_value ADDRESS_1 "$SWIFT_MOBILE_XCCONFIG")" "650 King Street")" + ADDRESS_2_VALUE="$(root_or_source_value ADDRESS_2 "$(env_fallback ADDRESS_2)" "$(read_env_value ADDRESS_2 "$REACT_NATIVE_ENV")" "$(read_env_value ADDRESS_2 "$SWIFT_MOBILE_XCCONFIG")" "Shopify HQ")" + CITY_VALUE="$(root_or_source_value CITY "$(env_fallback CITY)" "$(read_env_value CITY "$REACT_NATIVE_ENV")" "$(read_env_value CITY "$SWIFT_MOBILE_XCCONFIG")" "Toronto")" + COMPANY_VALUE="$(root_or_source_value COMPANY "$(env_fallback COMPANY)" "$(read_env_value COMPANY "$REACT_NATIVE_ENV")" "$(read_env_value COMPANY "$SWIFT_MOBILE_XCCONFIG")" "Shopify")" + COUNTRY_VALUE="$(root_or_source_value COUNTRY "$(env_fallback COUNTRY)" "$(read_env_value COUNTRY "$REACT_NATIVE_ENV")" "$(read_env_value COUNTRY "$SWIFT_MOBILE_XCCONFIG")" "CA")" + FIRST_NAME_VALUE="$(root_or_source_value FIRST_NAME "$(env_fallback FIRST_NAME)" "$(read_env_value FIRST_NAME "$REACT_NATIVE_ENV")" "$(read_env_value FIRST_NAME "$SWIFT_MOBILE_XCCONFIG")" "Evelyn")" + LAST_NAME_VALUE="$(root_or_source_value LAST_NAME "$(env_fallback LAST_NAME)" "$(read_env_value LAST_NAME "$REACT_NATIVE_ENV")" "$(read_env_value LAST_NAME "$SWIFT_MOBILE_XCCONFIG")" "Hartley")" + PROVINCE_VALUE="$(root_or_source_value PROVINCE "$(env_fallback PROVINCE)" "$(read_env_value PROVINCE "$REACT_NATIVE_ENV")" "$(read_env_value PROVINCE "$SWIFT_MOBILE_XCCONFIG")" "ON")" + ZIP_VALUE="$(root_or_source_value ZIP "$(env_fallback ZIP)" "$(read_env_value ZIP "$REACT_NATIVE_ENV")" "$(read_env_value ZIP "$SWIFT_MOBILE_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_MOBILE_XCCONFIG")" "+14165550100")" +} + +collect_missing_values() { + fill_optional_values_from_environment || true + + if is_missing_required_value "$STOREFRONT_DOMAIN_VALUE"; then + STOREFRONT_DOMAIN_VALUE="$(prompt_required "Storefront domain")" + fi + + if is_missing_required_value "$STOREFRONT_ACCESS_TOKEN_VALUE"; then + STOREFRONT_ACCESS_TOKEN_VALUE="$(prompt_required "Storefront access token")" + fi + + if [[ "$prompt_optional_values" == "true" && -z "$STOREFRONT_MERCHANT_IDENTIFIER_VALUE" ]]; then + STOREFRONT_MERCHANT_IDENTIFIER_VALUE="$(prompt_optional "Apple Pay merchant identifier")" + fi + + if [[ "$prompt_optional_values" == "true" && -z "$CUSTOMER_ACCOUNT_API_CLIENT_ID_VALUE" ]]; then + CUSTOMER_ACCOUNT_API_CLIENT_ID_VALUE="$(prompt_optional "Customer Account API client ID")" + fi + + if [[ "$prompt_optional_values" == "true" && -z "$CUSTOMER_ACCOUNT_API_SHOP_ID_VALUE" ]]; then + CUSTOMER_ACCOUNT_API_SHOP_ID_VALUE="$(prompt_optional "Customer Account API shop ID")" + fi +} + +write_env_assignment() { + printf '%s=%s\n' "$1" "$2" +} + +write_quoted_env_assignment() { + local value="${2//\"/\\\"}" + printf '%s="%s"\n' "$1" "$value" +} + +write_xcconfig_assignment() { + printf '%s = %s\n' "$1" "$2" +} + +generate_root_env() { + cat <&2 + exit 1 + fi + + if is_missing_required_value "$(read_env_value STOREFRONT_DOMAIN "$ROOT_ENV")" || + is_missing_required_value "$(read_env_value STOREFRONT_ACCESS_TOKEN "$ROOT_ENV")"; then + echo "Root .env is missing required storefront configuration." >&2 + exit 1 + fi + + if ! root_has_canonical_keys; then + echo "Root .env is missing canonical storefront configuration keys." >&2 + exit 1 + fi + + if customer_account_api_version_needs_default; then + echo "Root .env has a blank Customer Account API version." >&2 + exit 1 + fi + + return 0 + fi + + local root_needs_write="false" + if [[ ! -f "$ROOT_ENV" ]]; then + root_needs_write="true" + echo "Creating root storefront configuration at .env." + elif is_missing_required_value "$(read_env_value STOREFRONT_DOMAIN "$ROOT_ENV")" || + is_missing_required_value "$(read_env_value STOREFRONT_ACCESS_TOKEN "$ROOT_ENV")"; then + root_needs_write="true" + echo "Updating root storefront configuration at .env." + elif ! root_has_canonical_keys; then + root_needs_write="true" + echo "Normalizing root storefront configuration at .env." + elif customer_account_api_version_needs_default; then + root_needs_write="true" + echo "Normalizing root storefront configuration at .env." + fi + + if [[ "$root_needs_write" == "true" ]]; then + collect_missing_values + generate_root_env >"$ROOT_ENV" + elif fill_optional_values_from_environment; then + echo "Updating optional storefront configuration at .env." + generate_root_env >"$ROOT_ENV" + elif optional_values_need_prompt; then + echo "Updating optional storefront configuration at .env." + collect_missing_values + generate_root_env >"$ROOT_ENV" + fi + + load_values +} + +write_generated_files() { + generate_android_env >"$ANDROID_ENV" + generate_swift_mobile_xcconfig >"$SWIFT_MOBILE_XCCONFIG" + generate_swift_accelerated_xcconfig >"$SWIFT_ACCELERATED_XCCONFIG" + generate_react_native_env >"$REACT_NATIVE_ENV" + + echo "Synced sample app storefront configuration files." +} + +check_generated_file() { + local path="$1" + local generator="$2" + local expected + + expected="$(mktemp)" + "$generator" >"$expected" + + if [[ ! -f "$path" ]] || ! cmp -s "$path" "$expected"; then + rm -f "$expected" + echo "Missing or stale storefront configuration: ${path#"$ROOT_DIR/"}" >&2 + return 1 + fi + + rm -f "$expected" + return 0 +} + +check_generated_files() { + local failed="false" + + check_generated_file "$ANDROID_ENV" generate_android_env || failed="true" + check_generated_file "$SWIFT_MOBILE_XCCONFIG" generate_swift_mobile_xcconfig || failed="true" + check_generated_file "$SWIFT_ACCELERATED_XCCONFIG" generate_swift_accelerated_xcconfig || failed="true" + check_generated_file "$REACT_NATIVE_ENV" generate_react_native_env || failed="true" + + if [[ "$failed" == "true" ]]; then + exit 1 + fi + + echo "Sample app storefront configuration is up to date." +} + +ensure_root_env + +if [[ "$mode" == "check" ]]; then + check_generated_files +else + write_generated_files +fi diff --git a/scripts/test_setup_storefront_env b/scripts/test_setup_storefront_env new file mode 100755 index 00000000..698bbceb --- /dev/null +++ b/scripts/test_setup_storefront_env @@ -0,0 +1,278 @@ +#!/usr/bin/env bash + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +fixtures=() + +cleanup() { + local fixture + if [[ "${#fixtures[@]}" -eq 0 ]]; then + return + fi + + for fixture in "${fixtures[@]}"; do + rm -rf "$fixture" + done +} + +trap cleanup EXIT + +fail() { + echo "test_setup_storefront_env: $1" >&2 + exit 1 +} + +make_fixture() { + local fixture + fixture="$(mktemp -d "${TMPDIR:-/tmp}/checkout-kit-storefront-env.XXXXXX")" + fixtures+=("$fixture") + + mkdir -p \ + "$fixture/scripts" \ + "$fixture/platforms/android/samples/MobileBuyIntegration" \ + "$fixture/platforms/react-native/sample" \ + "$fixture/platforms/swift/Samples/MobileBuyIntegration" \ + "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp" + + cp "$REPO_ROOT/scripts/setup_storefront_env" "$fixture/scripts/setup_storefront_env" + chmod +x "$fixture/scripts/setup_storefront_env" + + printf '%s\n' "$fixture" +} + +assert_file_exists() { + [[ -f "$1" ]] || fail "expected generated file is missing" +} + +assert_contains() { + local path="$1" + local pattern="$2" + + grep -Fq "$pattern" "$path" || fail "expected generated file content was not found: $path: $pattern" +} + +assert_not_contains() { + local path="$1" + local pattern="$2" + + if grep -Fq "$pattern" "$path"; then + fail "unexpected generated file content was found" + fi +} + +assert_output_is_sanitized() { + local output_path="$1" + local value + + for value in \ + synthetic-store.example.myshopify.com \ + synthetic-token \ + synthetic-merchant \ + synthetic-client \ + synthetic-shop \ + migrated-store.example.myshopify.com \ + migrated-token \ + migrated-client \ + migrated-shop; do + if grep -Fq "$value" "$output_path"; then + fail "command output included a configured value" + fi + done +} + +test_sync_and_check() { + local fixture output + fixture="$(make_fixture)" + output="$fixture/output.log" + + STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com \ + STOREFRONT_ACCESS_TOKEN=synthetic-token \ + STOREFRONT_MERCHANT_IDENTIFIER=synthetic-merchant \ + CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client \ + CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop \ + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + + assert_output_is_sanitized "$output" + assert_file_exists "$fixture/.env" + assert_file_exists "$fixture/platforms/android/samples/MobileBuyIntegration/.env" + assert_file_exists "$fixture/platforms/react-native/sample/.env" + assert_file_exists "$fixture/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig" + assert_file_exists "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" + assert_not_contains "$fixture/platforms/android/samples/MobileBuyIntegration/.env" "STOREFRONT_MERCHANT_IDENTIFIER" + assert_not_contains "$fixture/platforms/react-native/sample/.env" "CUSTOMER_ACCOUNT_API_VERSION" + assert_not_contains "$fixture/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig" "CUSTOMER_ACCOUNT_API_VERSION" + assert_not_contains "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" "STOREFRONT_MERCHANT_IDENTIFIER" + assert_not_contains "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" "CUSTOMER_ACCOUNT_API_CLIENT_ID" + assert_not_contains "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" "EMAIL" + + "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 + assert_output_is_sanitized "$output" + + awk ' + /^API_VERSION=/ { print "API_VERSION=2026-01"; next } + { print } + ' "$fixture/.env" >"$fixture/.env.next" + mv "$fixture/.env.next" "$fixture/.env" + + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + assert_output_is_sanitized "$output" + assert_contains "$fixture/platforms/android/samples/MobileBuyIntegration/.env" "API_VERSION=2026-01" + assert_contains "$fixture/platforms/react-native/sample/.env" "API_VERSION=\"2026-01\"" + assert_contains "$fixture/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig" "API_VERSION = 2026-01" + assert_contains "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" "API_VERSION = 2026-01" + + printf '%s\n' "stale" >"$fixture/platforms/android/samples/MobileBuyIntegration/.env" + if "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1; then + fail "stale generated config passed --check" + fi + assert_output_is_sanitized "$output" +} + +test_required_values_only() { + local fixture output + fixture="$(make_fixture)" + output="$fixture/output.log" + + STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com \ + STOREFRONT_ACCESS_TOKEN=synthetic-token \ + "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + + assert_output_is_sanitized "$output" + assert_file_exists "$fixture/.env" + assert_contains "$fixture/.env" "STOREFRONT_MERCHANT_IDENTIFIER=" + assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_CLIENT_ID=" + assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_SHOP_ID=" + assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + assert_contains "$fixture/platforms/android/samples/MobileBuyIntegration/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + assert_not_contains "$fixture/platforms/react-native/sample/.env" "CUSTOMER_ACCOUNT_API_VERSION" + assert_not_contains "$fixture/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig" "CUSTOMER_ACCOUNT_API_VERSION" + assert_not_contains "$fixture/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig" "CUSTOMER_ACCOUNT_API_VERSION" + + "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 + assert_output_is_sanitized "$output" +} + +test_optional_sync_updates_blank_optional_values_after_required_setup() { + local fixture output + fixture="$(make_fixture)" + output="$fixture/output.log" + + STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com \ + STOREFRONT_ACCESS_TOKEN=synthetic-token \ + "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + + assert_output_is_sanitized "$output" + assert_contains "$fixture/.env" "STOREFRONT_MERCHANT_IDENTIFIER=" + assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_CLIENT_ID=" + assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_SHOP_ID=" + + STOREFRONT_MERCHANT_IDENTIFIER=synthetic-merchant \ + CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client \ + CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop \ + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + + assert_output_is_sanitized "$output" + assert_contains "$fixture/.env" "STOREFRONT_MERCHANT_IDENTIFIER=synthetic-merchant" + assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client" + assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop" + assert_contains "$fixture/platforms/react-native/sample/.env" "STOREFRONT_MERCHANT_IDENTIFIER=\"synthetic-merchant\"" + assert_contains "$fixture/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig" "CUSTOMER_ACCOUNT_API_CLIENT_ID = synthetic-client" + assert_contains "$fixture/platforms/android/samples/MobileBuyIntegration/.env" "CUSTOMER_ACCOUNT_API_REDIRECT_URI=shop.synthetic-shop.app://callback" +} + +test_normalizes_existing_root_env() { + local fixture output + fixture="$(make_fixture)" + output="$fixture/output.log" + + cat >"$fixture/.env" <<'EOF' +STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=synthetic-token +EOF + + if "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1; then + fail "non-canonical root .env 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" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + assert_contains "$fixture/.env" "EMAIL=checkout-kit@example.com" + + "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 + assert_output_is_sanitized "$output" +} + +test_blank_customer_account_api_version_defaults() { + local fixture output + fixture="$(make_fixture)" + output="$fixture/output.log" + + cat >"$fixture/.env" <<'EOF' +STOREFRONT_DOMAIN=synthetic-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=synthetic-token +STOREFRONT_MERCHANT_IDENTIFIER= +API_VERSION=2026-04 +CUSTOMER_ACCOUNT_API_CLIENT_ID=synthetic-client +CUSTOMER_ACCOUNT_API_SHOP_ID=synthetic-shop +CUSTOMER_ACCOUNT_API_VERSION= +EMAIL=checkout-kit@example.com +ADDRESS_1=650 King Street +ADDRESS_2=Shopify HQ +CITY=Toronto +COMPANY=Shopify +COUNTRY=CA +FIRST_NAME=Evelyn +LAST_NAME=Hartley +PROVINCE=ON +ZIP=M5V 1M7 +PHONE=+14165550100 +EOF + + "$fixture/scripts/setup_storefront_env" --skip-optional-prompts >"$output" 2>&1 + assert_output_is_sanitized "$output" + + assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + assert_contains "$fixture/platforms/android/samples/MobileBuyIntegration/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + assert_contains "$fixture/platforms/android/samples/MobileBuyIntegration/.env" "/2026-04/graphql" +} + +test_migration_from_platform_config() { + local fixture output android_env + fixture="$(make_fixture)" + output="$fixture/output.log" + android_env="$fixture/platforms/android/samples/MobileBuyIntegration/.env" + + cat >"$android_env" <<'EOF' +STOREFRONT_DOMAIN=migrated-store.example.myshopify.com +STOREFRONT_ACCESS_TOKEN=migrated-token +CUSTOMER_ACCOUNT_API_CLIENT_ID=migrated-client +CUSTOMER_ACCOUNT_API_REDIRECT_URI=shop.migrated-shop.app://callback +CUSTOMER_ACCOUNT_API_GRAPHQL_BASE_URL=https://shopify.com/migrated-shop/account/customer/api/2026-04/graphql +EMAIL=migrated@example.com +PHONE=+15555550100 +EOF + + "$fixture/scripts/setup_storefront_env" >"$output" 2>&1 + assert_output_is_sanitized "$output" + + assert_file_exists "$fixture/.env" + assert_contains "$fixture/.env" "API_VERSION=2026-04" + assert_contains "$fixture/.env" "CUSTOMER_ACCOUNT_API_VERSION=2026-04" + + "$fixture/scripts/setup_storefront_env" --check >"$output" 2>&1 + assert_output_is_sanitized "$output" +} + +test_sync_and_check +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_migration_from_platform_config + +echo "setup_storefront_env synthetic tests passed."