From f8b72c3db4d91e395b2ac1fd05e4ab184f9836d1 Mon Sep 17 00:00:00 2001 From: Kyle Schellen Date: Wed, 20 May 2026 19:29:43 -0400 Subject: [PATCH] Centralize sample storefront configuration --- .env.example | 32 + .github/workflows/android-test.yml | 6 +- .github/workflows/rn-test-android.yml | 4 +- .github/workflows/swift-build-samples.yml | 1 + .github/workflows/swift-test-workflow.yml | 11 + .../samples/MobileBuyIntegration/.env.example | 13 +- .../MobileBuyIntegration/app/build.gradle | 23 +- .../android/scripts/apollo_download_schema | 96 ++- platforms/android/scripts/setup_env.sh | 56 +- platforms/react-native/README.md | 4 +- .../__mocks__/react-native-config.ts | 1 + platforms/react-native/sample/.env.example | 23 +- platforms/react-native/sample/@types/env.d.ts | 4 +- platforms/react-native/sample/src/App.tsx | 23 +- .../Scripts/generate_entitlements.sh | 2 +- .../Storefront.xcconfig.example | 32 +- .../Storefront.xcconfig.example | 7 +- platforms/swift/Scripts/build_samples | 21 +- .../swift/Scripts/ensure_storefront_config | 13 +- scripts/setup_storefront_env | 748 ++++++++++++++++++ scripts/test_setup_storefront_env | 278 +++++++ 21 files changed, 1235 insertions(+), 163 deletions(-) create mode 100644 .env.example create mode 100755 scripts/setup_storefront_env create mode 100755 scripts/test_setup_storefront_env diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..4693ace4 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# 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 +STOREFRONT_MERCHANT_IDENTIFIER= + +# Storefront API version +API_VERSION=2025-07 + +# Customer Account API (optional) +CUSTOMER_ACCOUNT_API_CLIENT_ID= +CUSTOMER_ACCOUNT_API_SHOP_ID= +CUSTOMER_ACCOUNT_API_VERSION=unstable + +# 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/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 index e4b3721a..a3e5d264 100644 --- a/platforms/android/samples/MobileBuyIntegration/.env.example +++ b/platforms/android/samples/MobileBuyIntegration/.env.example @@ -1,14 +1,15 @@ +# Platform-local example for the Android sample. Prefer the repo-root +# .env.example for shared setup, then run scripts/setup_storefront_env. STOREFRONT_DOMAIN= STOREFRONT_ACCESS_TOKEN= API_VERSION=2025-07 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/ +# Android derives Customer Account redirect and API URLs from the shop ID and version. +CUSTOMER_ACCOUNT_API_SHOP_ID= +CUSTOMER_ACCOUNT_API_VERSION=unstable # 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 +EMAIL=checkout-kit@example.com +PHONE=+14165550100 diff --git a/platforms/android/samples/MobileBuyIntegration/app/build.gradle b/platforms/android/samples/MobileBuyIntegration/app/build.gradle index d0d76b6a..091b89b0 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", "2025-07") // 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/scripts/apollo_download_schema b/platforms/android/scripts/apollo_download_schema index 9a545d85..436bb01b 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=2025-07 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=2025-07 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/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 index 0f30fe9d..dd5a79ca 100644 --- a/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig.example +++ b/platforms/swift/Samples/MobileBuyIntegration/Storefront.xcconfig.example @@ -1,29 +1,29 @@ // --- Storefront // --- Find your Storefront API access token under: // ----- https://admin.shopify.com/store/{STOREFRONT}/settings/apps/development/{APP_ID}/api_credentials +// --- Prefer the repo-root .env.example for shared setup, then run scripts/setup_storefront_env. -STOREFRONT_DOMAIN = -STOREFRONT_ACCESS_TOKEN = -STOREFRONT_MERCHANT_IDENTIFIER = +STOREFRONT_DOMAIN = your-store.myshopify.com +STOREFRONT_ACCESS_TOKEN = your-public-storefront-access-token +STOREFRONT_MERCHANT_IDENTIFIER = +API_VERSION = 2025-07 // --- 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 = +CUSTOMER_ACCOUNT_API_CLIENT_ID = +CUSTOMER_ACCOUNT_API_SHOP_ID = // --- Buyer preference data -EMAIL = test-checkout-sdk@shopify.com +EMAIL = checkout-kit@example.com -FIRST_NAME = Test -LAST_NAME = Test +FIRST_NAME = Evelyn +LAST_NAME = Hartley -ADDRESS_1 = The Cloak & Dagger -ADDRESS_2 = 1st Street Southeast -CITY = Calgary +ADDRESS_1 = 650 King Street +ADDRESS_2 = Shopify HQ +CITY = Toronto COUNTRY = CA -PROVINCE = AB -ZIP = T1X 0L3 -PHONE = +155565708743 - -API_VERSION = 2025-07 +PROVINCE = ON +ZIP = M5V 1M7 +PHONE = +14165550100 diff --git a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig.example b/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig.example index 44618266..791e21a2 100644 --- a/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig.example +++ b/platforms/swift/Samples/ShopifyAcceleratedCheckoutsApp/Storefront.xcconfig.example @@ -1,10 +1,11 @@ // --- Storefront // --- Find your Storefront API access token under: // ----- https://admin.shopify.com/store/{STOREFRONT}/settings/apps/development/{APP_ID}/api_credentials +// --- Prefer the repo-root .env.example for shared setup, then run scripts/setup_storefront_env. // 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 = 2025-07 \ No newline at end of file +STOREFRONT_DOMAIN = your-store.myshopify.com +STOREFRONT_ACCESS_TOKEN = your-public-storefront-access-token +API_VERSION = 2025-07 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..20569492 --- /dev/null +++ b/scripts/setup_storefront_env @@ -0,0 +1,748 @@ +#!/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="2025-07" +DEFAULT_CUSTOMER_ACCOUNT_API_VERSION="unstable" + +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..4f2f4f5e --- /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=unstable" + assert_contains "$fixture/platforms/android/samples/MobileBuyIntegration/.env" "CUSTOMER_ACCOUNT_API_VERSION=unstable" + 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=unstable" + 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=2025-07 +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=unstable" + assert_contains "$fixture/platforms/android/samples/MobileBuyIntegration/.env" "CUSTOMER_ACCOUNT_API_VERSION=unstable" + assert_contains "$fixture/platforms/android/samples/MobileBuyIntegration/.env" "/unstable/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=2025-07" + 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."