@daemux/store-automator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +122 -0
  4. package/bin/cli.mjs +77 -0
  5. package/package.json +33 -0
  6. package/plugins/store-automator/.claude-plugin/plugin.json +9 -0
  7. package/plugins/store-automator/agents/appstore-media-designer.md +227 -0
  8. package/plugins/store-automator/agents/appstore-meta-creator.md +185 -0
  9. package/plugins/store-automator/agents/appstore-reviewer.md +180 -0
  10. package/src/dependency-check.mjs +26 -0
  11. package/src/install.mjs +140 -0
  12. package/src/mcp-setup.mjs +93 -0
  13. package/src/prompt.mjs +55 -0
  14. package/src/settings.mjs +106 -0
  15. package/src/templates.mjs +55 -0
  16. package/src/uninstall.mjs +100 -0
  17. package/src/utils.mjs +46 -0
  18. package/templates/CLAUDE.md.template +219 -0
  19. package/templates/Gemfile.template +2 -0
  20. package/templates/ci.config.yaml.template +51 -0
  21. package/templates/codemagic.template.yaml +289 -0
  22. package/templates/fastlane/android/Appfile.template +2 -0
  23. package/templates/fastlane/android/Fastfile.template +36 -0
  24. package/templates/fastlane/android/Pluginfile.template +1 -0
  25. package/templates/fastlane/app_rating_config.json.template +17 -0
  26. package/templates/fastlane/iap_config.json.template +53 -0
  27. package/templates/fastlane/ios/Appfile.template +2 -0
  28. package/templates/fastlane/ios/Deliverfile.template +1 -0
  29. package/templates/fastlane/ios/Fastfile.template +47 -0
  30. package/templates/fastlane/ios/Pluginfile.template +1 -0
  31. package/templates/fastlane/ios/Snapfile.template +26 -0
  32. package/templates/scripts/check_changed.sh +23 -0
  33. package/templates/scripts/check_google_play.py +139 -0
  34. package/templates/scripts/generate.sh +77 -0
  35. package/templates/scripts/manage_version_ios.py +168 -0
  36. package/templates/web/deploy-cloudflare.mjs +240 -0
  37. package/templates/web/marketing.html +121 -0
  38. package/templates/web/privacy.html +119 -0
  39. package/templates/web/styles.css +377 -0
  40. package/templates/web/support.html +156 -0
  41. package/templates/web/terms.html +101 -0
@@ -0,0 +1,289 @@
1
+ workflows:
2
+ ios-release:
3
+ name: iOS Release
4
+ max_build_duration: 90
5
+ instance_type: mac_mini_m2
6
+ environment:
7
+ flutter: stable
8
+ xcode: latest
9
+ vars:
10
+ BUNDLE_ID: "${BUNDLE_ID}"
11
+ APP_NAME: "${APP_NAME}"
12
+ SKU: "${SKU}"
13
+ APPLE_ID: "${APPLE_ID}"
14
+ P8_KEY_PATH: "${P8_KEY_PATH}"
15
+ APPLE_KEY_ID: "${APPLE_KEY_ID}"
16
+ APPLE_ISSUER_ID: "${APPLE_ISSUER_ID}"
17
+ PRIMARY_CATEGORY: "${PRIMARY_CATEGORY}"
18
+ SECONDARY_CATEGORY: "${SECONDARY_CATEGORY}"
19
+ PRICE_TIER: "${PRICE_TIER}"
20
+ SUBMIT_FOR_REVIEW: "${SUBMIT_FOR_REVIEW}"
21
+ AUTOMATIC_RELEASE: "${AUTOMATIC_RELEASE}"
22
+ ios_signing:
23
+ distribution_type: app_store
24
+ bundle_identifier: "${BUNDLE_ID}"
25
+ cache:
26
+ cache_paths:
27
+ - $HOME/.codemagic_keys
28
+ - $HOME/.gem
29
+ - ios/vendor/bundle
30
+ triggering:
31
+ events:
32
+ - push
33
+ branch_patterns:
34
+ - pattern: main
35
+ include: true
36
+ scripts:
37
+ - name: Ensure CERTIFICATE_PRIVATE_KEY
38
+ script: |
39
+ KEY_FILE="$HOME/.codemagic_keys/ios_dist_private_key"
40
+ mkdir -p "$HOME/.codemagic_keys"
41
+ if [ -f "$KEY_FILE" ]; then
42
+ echo "Reusing cached CERTIFICATE_PRIVATE_KEY"
43
+ else
44
+ echo "Generating new CERTIFICATE_PRIVATE_KEY"
45
+ ssh-keygen -t rsa -b 2048 -m PEM -f "$KEY_FILE" -q -N ""
46
+ fi
47
+ echo "CERTIFICATE_PRIVATE_KEY<<DELIMITER" >> $CM_ENV
48
+ cat "$KEY_FILE" >> $CM_ENV
49
+ echo "DELIMITER" >> $CM_ENV
50
+
51
+ - name: Set up App Store Connect API key
52
+ script: |
53
+ echo "APP_STORE_CONNECT_KEY_IDENTIFIER=$APPLE_KEY_ID" >> $CM_ENV
54
+ echo "APP_STORE_CONNECT_ISSUER_ID=$APPLE_ISSUER_ID" >> $CM_ENV
55
+ echo "APP_STORE_CONNECT_PRIVATE_KEY<<KEYDELIMITER" >> $CM_ENV
56
+ cat "$CM_BUILD_DIR/$P8_KEY_PATH" >> $CM_ENV
57
+ echo "KEYDELIMITER" >> $CM_ENV
58
+
59
+ - name: Set up iOS code signing
60
+ script: |
61
+ app-store-connect fetch-signing-files "$BUNDLE_ID" \
62
+ --type IOS_APP_STORE --create
63
+ keychain initialize
64
+ app-store-connect certificates list \
65
+ --type IOS_DISTRIBUTION \
66
+ --certificate-key=@env:CERTIFICATE_PRIVATE_KEY --save
67
+ keychain add-certificates
68
+ xcode-project use-profiles
69
+
70
+ - name: Manage iOS version
71
+ script: |
72
+ pip3 install PyJWT cryptography requests
73
+ VERSION_JSON=$(python3 scripts/manage_version_ios.py)
74
+ APP_VERSION=$(echo "$VERSION_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['version'])")
75
+ APP_VERSION_ID=$(echo "$VERSION_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['version_id'])")
76
+ APP_STATUS=$(echo "$VERSION_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['state'])")
77
+ echo "APP_VERSION=$APP_VERSION" >> $CM_ENV
78
+ echo "APP_VERSION_ID=$APP_VERSION_ID" >> $CM_ENV
79
+ echo "APP_STATUS=$APP_STATUS" >> $CM_ENV
80
+ echo "iOS version: $APP_VERSION (state: $APP_STATUS)"
81
+
82
+ - name: Set Flutter version
83
+ script: |
84
+ BUILD_NUMBER=$(($(app-store-connect get-latest-app-store-build-number "$BUNDLE_ID" 2>/dev/null || echo "0") + 1))
85
+ sed -i '' "s/^version:.*/version: ${APP_VERSION}+${BUILD_NUMBER}/" pubspec.yaml
86
+ echo "Building: $APP_VERSION+$BUILD_NUMBER"
87
+
88
+ - name: Flutter packages
89
+ script: flutter pub get
90
+
91
+ - name: Build iOS
92
+ script: flutter build ipa --release --export-options-plist=/tmp/export.plist
93
+
94
+ - name: Install Fastlane
95
+ script: |
96
+ cd ios
97
+ gem install bundler
98
+ bundle install
99
+
100
+ - name: Create app record (idempotent)
101
+ script: |
102
+ cd ios
103
+ bundle exec fastlane produce create \
104
+ -u "$APPLE_ID" \
105
+ -a "$BUNDLE_ID" \
106
+ --app_name "$APP_NAME" \
107
+ --sku "$SKU" \
108
+ || true
109
+
110
+ - name: Deploy to App Store
111
+ script: |
112
+ cd ios
113
+ bundle exec fastlane deploy_ios
114
+
115
+ - name: Sync IAP and subscriptions
116
+ script: |
117
+ if ./scripts/check_changed.sh fastlane/iap_config.json; then
118
+ cd ios
119
+ bundle exec fastlane sync_iap
120
+ else
121
+ echo "IAP config unchanged - skipping"
122
+ fi
123
+
124
+ artifacts:
125
+ - build/ios/ipa/*.ipa
126
+ - /tmp/xcodebuild_logs/*.log
127
+
128
+ android-release:
129
+ name: Android Release
130
+ max_build_duration: 60
131
+ instance_type: mac_mini_m2
132
+ environment:
133
+ flutter: stable
134
+ vars:
135
+ PACKAGE_NAME: "${PACKAGE_NAME}"
136
+ APP_NAME: "${APP_NAME}"
137
+ BUNDLE_ID: "${BUNDLE_ID}"
138
+ GOOGLE_SA_JSON_PATH: "${GOOGLE_SA_JSON_PATH}"
139
+ KEYSTORE_PASSWORD: "${KEYSTORE_PASSWORD}"
140
+ TRACK: "${TRACK}"
141
+ ROLLOUT_FRACTION: "${ROLLOUT_FRACTION}"
142
+ IN_APP_UPDATE_PRIORITY: "${IN_APP_UPDATE_PRIORITY}"
143
+ APPLE_KEY_ID: "${APPLE_KEY_ID}"
144
+ APPLE_ISSUER_ID: "${APPLE_ISSUER_ID}"
145
+ P8_KEY_PATH: "${P8_KEY_PATH}"
146
+ cache:
147
+ cache_paths:
148
+ - $HOME/.gem
149
+ - $HOME/.gradle/caches
150
+ - android/vendor/bundle
151
+ triggering:
152
+ events:
153
+ - push
154
+ branch_patterns:
155
+ - pattern: main
156
+ include: true
157
+ scripts:
158
+ - name: Ensure Android upload keystore
159
+ script: |
160
+ KEYSTORE_PATH="$CM_BUILD_DIR/android/upload.keystore"
161
+ if [ -f "$KEYSTORE_PATH" ]; then
162
+ echo "Using existing upload keystore from repo"
163
+ else
164
+ echo "Generating new upload keystore..."
165
+ keytool -genkey -v \
166
+ -keystore "$KEYSTORE_PATH" \
167
+ -storetype JKS \
168
+ -keyalg RSA \
169
+ -keysize 2048 \
170
+ -validity 10000 \
171
+ -alias upload \
172
+ -storepass "$KEYSTORE_PASSWORD" \
173
+ -keypass "$KEYSTORE_PASSWORD" \
174
+ -dname "CN=Upload Key, O=Developer, C=US"
175
+ cd "$CM_BUILD_DIR"
176
+ git add android/upload.keystore
177
+ git commit -m "chore: add Android upload keystore [skip ci]"
178
+ git push origin HEAD
179
+ echo "Upload keystore generated and committed to repo"
180
+ fi
181
+ echo "CM_KEYSTORE_PATH=$KEYSTORE_PATH" >> $CM_ENV
182
+ echo "CM_KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD" >> $CM_ENV
183
+ echo "CM_KEY_ALIAS=upload" >> $CM_ENV
184
+ echo "CM_KEY_PASSWORD=$KEYSTORE_PASSWORD" >> $CM_ENV
185
+
186
+ - name: Check Google Play readiness
187
+ script: |
188
+ echo "Checking Google Play setup status..."
189
+ pip3 install PyJWT cryptography requests
190
+ export SA_JSON="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
191
+ export PACKAGE_NAME="$PACKAGE_NAME"
192
+ RESULT=$(python3 scripts/check_google_play.py)
193
+ READY=$(echo "$RESULT" | python3 -c "import sys,json; print(str(json.load(sys.stdin)['ready']).lower())")
194
+ if [ "$READY" != "true" ]; then
195
+ echo "GOOGLE_PLAY_READY=false" >> $CM_ENV
196
+ MISSING_STEPS=$(echo "$RESULT" | python3 -c "import sys,json; print('\n'.join(json.load(sys.stdin)['missing_steps']))")
197
+ cat > $CM_BUILD_DIR/HOW_TO_GOOGLE_PLAY.md << GUIDE
198
+ # Google Play Setup - Remaining Manual Steps
199
+
200
+ Your Android AAB is in the build artifacts. Complete these steps, then push again:
201
+
202
+ $MISSING_STEPS
203
+
204
+ ## After completing all steps:
205
+ Just \`git push\` again - Codemagic will publish automatically.
206
+ GUIDE
207
+ echo "Google Play not ready - see HOW_TO_GOOGLE_PLAY.md in artifacts"
208
+ else
209
+ echo "GOOGLE_PLAY_READY=true" >> $CM_ENV
210
+ echo "Google Play ready for automated publishing"
211
+ fi
212
+
213
+ - name: Manage Android version
214
+ script: |
215
+ # Read iOS version for consistency (if iOS workflow ran)
216
+ pip3 install PyJWT cryptography requests
217
+ echo "APP_STORE_CONNECT_KEY_IDENTIFIER=$APPLE_KEY_ID" >> $CM_ENV
218
+ echo "APP_STORE_CONNECT_ISSUER_ID=$APPLE_ISSUER_ID" >> $CM_ENV
219
+ echo "APP_STORE_CONNECT_PRIVATE_KEY<<KEYDELIMITER" >> $CM_ENV
220
+ cat "$CM_BUILD_DIR/$P8_KEY_PATH" >> $CM_ENV
221
+ echo "KEYDELIMITER" >> $CM_ENV
222
+ VERSION_JSON=$(python3 scripts/manage_version_ios.py)
223
+ APP_VERSION=$(echo "$VERSION_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin)['version'])")
224
+ echo "APP_VERSION=$APP_VERSION" >> $CM_ENV
225
+ if [ "$GOOGLE_PLAY_READY" != "true" ]; then
226
+ LATEST_BUILD=0
227
+ else
228
+ LATEST_BUILD=$(google-play get-latest-build-number \
229
+ --package-name "$PACKAGE_NAME" \
230
+ --tracks=production,beta,alpha,internal 2>/dev/null || echo "0")
231
+ fi
232
+ NEW_BUILD=$(($LATEST_BUILD + 1))
233
+ sed -i '' "s/^version:.*/version: ${APP_VERSION}+${NEW_BUILD}/" pubspec.yaml
234
+ echo "ANDROID_VERSION_CODE=$NEW_BUILD" >> $CM_ENV
235
+ echo "Android versionCode: $NEW_BUILD, versionName: $APP_VERSION"
236
+
237
+ - name: Flutter packages
238
+ script: flutter pub get
239
+
240
+ - name: Build Android
241
+ script: flutter build appbundle --release
242
+
243
+ - name: Install Fastlane
244
+ script: |
245
+ cd android
246
+ gem install bundler
247
+ bundle install
248
+
249
+ - name: Deploy to Google Play
250
+ script: |
251
+ if [ "$GOOGLE_PLAY_READY" != "true" ]; then
252
+ echo "Skipping - Google Play setup incomplete"
253
+ exit 0
254
+ fi
255
+ export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
256
+ cd android
257
+ bundle exec fastlane deploy_android
258
+
259
+ - name: Sync subscriptions and IAP
260
+ script: |
261
+ if [ "$GOOGLE_PLAY_READY" != "true" ]; then
262
+ echo "Skipping - Google Play setup incomplete"
263
+ exit 0
264
+ fi
265
+ if ./scripts/check_changed.sh fastlane/iap_config.json; then
266
+ export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
267
+ cd android
268
+ bundle exec fastlane sync_google_iap
269
+ else
270
+ echo "IAP config unchanged - skipping"
271
+ fi
272
+
273
+ - name: Update data safety form
274
+ script: |
275
+ if [ "$GOOGLE_PLAY_READY" != "true" ]; then
276
+ echo "Skipping - Google Play setup incomplete"
277
+ exit 0
278
+ fi
279
+ if ./scripts/check_changed.sh fastlane/data_safety.csv; then
280
+ export GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$CM_BUILD_DIR/$GOOGLE_SA_JSON_PATH"
281
+ cd android
282
+ bundle exec fastlane update_data_safety
283
+ else
284
+ echo "Data safety unchanged - skipping"
285
+ fi
286
+
287
+ artifacts:
288
+ - build/app/outputs/**/*.aab
289
+ - $CM_BUILD_DIR/HOW_TO_GOOGLE_PLAY.md
@@ -0,0 +1,2 @@
1
+ package_name ENV["PACKAGE_NAME"]
2
+ json_key_file ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH"]
@@ -0,0 +1,36 @@
1
+ default_platform(:android)
2
+
3
+ def metadata_changed?(path)
4
+ !sh("git diff --name-only HEAD~1 -- #{path}").strip.empty?
5
+ rescue StandardError
6
+ true
7
+ end
8
+
9
+ platform :android do
10
+ lane :deploy_android do
11
+ upload_to_play_store(
12
+ aab: "../build/app/outputs/bundle/release/app-release.aab",
13
+ track: ENV.fetch("TRACK", "internal"),
14
+ json_key: ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH"],
15
+ skip_upload_metadata: !metadata_changed?("fastlane/metadata/android/"),
16
+ skip_upload_screenshots: !metadata_changed?("fastlane/screenshots/android/"),
17
+ skip_upload_images: !metadata_changed?("fastlane/screenshots/android/"),
18
+ skip_upload_changelogs: false,
19
+ metadata_path: "../fastlane/metadata/android",
20
+ rollout: ENV.fetch("ROLLOUT_FRACTION", "").empty? ? nil : ENV["ROLLOUT_FRACTION"].to_f,
21
+ in_app_update_priority: ENV.fetch("IN_APP_UPDATE_PRIORITY", "3").to_i
22
+ )
23
+ end
24
+
25
+ lane :sync_google_iap do
26
+ manage_google_iap(
27
+ json_key: ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH"],
28
+ package_name: ENV["PACKAGE_NAME"],
29
+ config_path: "../fastlane/iap_config.json"
30
+ )
31
+ end
32
+
33
+ lane :update_data_safety do
34
+ sh("python3", "../scripts/update_data_safety.py") if File.exist?("../fastlane/data_safety.csv")
35
+ end
36
+ end
@@ -0,0 +1 @@
1
+ gem "fastlane-plugin-iap", git: "https://github.com/daemux/fastlane-plugin-iap"
@@ -0,0 +1,17 @@
1
+ {
2
+ "alcoholTobaccoOrDrugUseOrReferences": "NONE",
3
+ "contests": "NONE",
4
+ "gamblingSimulated": "NONE",
5
+ "medicalOrTreatmentInformation": "NONE",
6
+ "profanityOrCrudeHumor": "NONE",
7
+ "sexualContentGraphicAndNudity": "NONE",
8
+ "sexualContentOrNudity": "NONE",
9
+ "horrorOrFearThemes": "NONE",
10
+ "matureOrSuggestiveThemes": "NONE",
11
+ "unrestrictedWebAccess": false,
12
+ "violenceCartoonOrFantasy": "NONE",
13
+ "violenceRealisticProlongedGraphicOrSadistic": "NONE",
14
+ "violenceRealistic": "NONE",
15
+ "gamblingAndContests": false,
16
+ "seventeenPlus": false
17
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "subscription_groups": [
3
+ {
4
+ "reference_name": "Premium",
5
+ "localizations": {
6
+ "en-US": { "name": "Premium", "custom_name": "Premium Plans" }
7
+ },
8
+ "subscriptions": [
9
+ {
10
+ "product_id": "${BUNDLE_ID}.premium.monthly",
11
+ "reference_name": "Premium Monthly",
12
+ "duration": "ONE_MONTH",
13
+ "group_level": 1,
14
+ "localizations": {
15
+ "en-US": {
16
+ "name": "Monthly",
17
+ "description": "Full access, billed monthly"
18
+ }
19
+ },
20
+ "prices": {
21
+ "USD": "9.99"
22
+ },
23
+ "introductory_offer": {
24
+ "type": "FREE",
25
+ "duration": "ONE_WEEK",
26
+ "periods": 1
27
+ }
28
+ },
29
+ {
30
+ "product_id": "${BUNDLE_ID}.premium.yearly",
31
+ "reference_name": "Premium Yearly",
32
+ "duration": "ONE_YEAR",
33
+ "group_level": 1,
34
+ "localizations": {
35
+ "en-US": {
36
+ "name": "Yearly",
37
+ "description": "Full access, billed yearly"
38
+ }
39
+ },
40
+ "prices": {
41
+ "USD": "69.99"
42
+ },
43
+ "introductory_offer": {
44
+ "type": "FREE",
45
+ "duration": "ONE_MONTH",
46
+ "periods": 1
47
+ }
48
+ }
49
+ ]
50
+ }
51
+ ],
52
+ "in_app_purchases": []
53
+ }
@@ -0,0 +1,2 @@
1
+ app_identifier ENV["BUNDLE_ID"]
2
+ apple_id ENV["APPLE_ID"]
@@ -0,0 +1 @@
1
+ app_identifier ENV["BUNDLE_ID"]
@@ -0,0 +1,47 @@
1
+ default_platform(:ios)
2
+
3
+ def metadata_changed?(path)
4
+ !sh("git diff --name-only HEAD~1 -- #{path}").strip.empty?
5
+ rescue StandardError
6
+ true
7
+ end
8
+
9
+ def asc_api_key
10
+ app_store_connect_api_key(
11
+ key_id: ENV["APP_STORE_CONNECT_KEY_IDENTIFIER"],
12
+ issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
13
+ key_content: ENV["APP_STORE_CONNECT_PRIVATE_KEY"],
14
+ is_key_content_base64: false
15
+ )
16
+ end
17
+
18
+ platform :ios do
19
+ lane :deploy_ios do
20
+ deliver(
21
+ api_key: asc_api_key,
22
+ ipa: "../build/ios/ipa/*.ipa",
23
+ skip_metadata: !metadata_changed?("fastlane/metadata/ios/"),
24
+ skip_screenshots: !metadata_changed?("fastlane/screenshots/ios/"),
25
+ sync_screenshots: true,
26
+ metadata_path: "../fastlane/metadata/ios",
27
+ screenshots_path: "../fastlane/screenshots/ios",
28
+ submit_for_review: ENV.fetch("SUBMIT_FOR_REVIEW", "true") == "true",
29
+ automatic_release: ENV.fetch("AUTOMATIC_RELEASE", "true") == "true",
30
+ force: true,
31
+ app_rating_config_path: "../fastlane/app_rating_config.json",
32
+ price_tier: ENV.fetch("PRICE_TIER", "0").to_i,
33
+ run_precheck_before_submit: true,
34
+ precheck_include_in_app_purchases: false,
35
+ primary_category: ENV.fetch("PRIMARY_CATEGORY", "UTILITIES"),
36
+ secondary_category: ENV.fetch("SECONDARY_CATEGORY", "PRODUCTIVITY")
37
+ )
38
+ end
39
+
40
+ lane :sync_iap do
41
+ manage_iap(
42
+ api_key: asc_api_key,
43
+ app_id: ENV["BUNDLE_ID"],
44
+ config_path: "../fastlane/iap_config.json"
45
+ )
46
+ end
47
+ end
@@ -0,0 +1 @@
1
+ gem "fastlane-plugin-iap", git: "https://github.com/daemux/fastlane-plugin-iap"
@@ -0,0 +1,26 @@
1
+ devices([
2
+ "iPhone 16 Pro Max",
3
+ "iPhone 16 Pro",
4
+ "iPad Pro 13-inch (M4)",
5
+ "iPad Pro 12.9-inch (6th generation)"
6
+ ])
7
+
8
+ languages([
9
+ "en-US",
10
+ "ja",
11
+ "de-DE",
12
+ "fr-FR",
13
+ "es-ES",
14
+ "pt-BR",
15
+ "zh-Hans",
16
+ "zh-Hant",
17
+ "ko",
18
+ "ru",
19
+ "ar-SA",
20
+ "hi"
21
+ ])
22
+
23
+ scheme(ENV.fetch("APP_SCHEME", "Runner"))
24
+ output_directory("../fastlane/screenshots/ios")
25
+ clear_previous_screenshots(false)
26
+ concurrent_simulators(true)
@@ -0,0 +1,23 @@
1
+ #!/bin/bash
2
+ # Usage: ./scripts/check_changed.sh <path>
3
+ # Exit 0 if changed, exit 1 if unchanged
4
+ # Used by codemagic.yaml for conditional metadata/screenshot uploads
5
+
6
+ set -euo pipefail
7
+
8
+ PATH_TO_CHECK="${1:-}"
9
+
10
+ if [ -z "$PATH_TO_CHECK" ]; then
11
+ echo "Usage: $0 <path>"
12
+ echo " Exit 0 if path has changes since last commit"
13
+ echo " Exit 1 if path is unchanged"
14
+ exit 2
15
+ fi
16
+
17
+ if git diff --name-only HEAD~1 -- "$PATH_TO_CHECK" | grep -q .; then
18
+ echo "CHANGED: $PATH_TO_CHECK"
19
+ exit 0
20
+ else
21
+ echo "UNCHANGED: $PATH_TO_CHECK"
22
+ exit 1
23
+ fi
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Check Google Play readiness for automated publishing.
4
+
5
+ Reads a service account JSON file and verifies that the app exists on Google Play,
6
+ has at least one uploaded bundle, and has completed track setup.
7
+
8
+ Required env vars:
9
+ SA_JSON - Path to Google service account JSON file
10
+ PACKAGE_NAME - Android package name (e.g. com.example.app)
11
+
12
+ Output:
13
+ Prints JSON with keys: ready (bool), missing_steps (list of strings)
14
+ Exit code 0 on success (even if not ready), non-zero on fatal error.
15
+ """
16
+ import json
17
+ import os
18
+ import sys
19
+ import time
20
+
21
+ try:
22
+ import jwt
23
+ import requests
24
+ except ImportError:
25
+ print("Installing dependencies...", file=sys.stderr)
26
+ import subprocess
27
+ subprocess.check_call(
28
+ [sys.executable, "-m", "pip", "install", "PyJWT", "cryptography", "requests"],
29
+ stdout=subprocess.DEVNULL,
30
+ )
31
+ import jwt
32
+ import requests
33
+
34
+ API_BASE = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications"
35
+ TIMEOUT = (10, 30)
36
+
37
+
38
+ def get_access_token(sa_path: str) -> str:
39
+ """Obtain an OAuth2 access token using the service account credentials."""
40
+ with open(sa_path, "r") as fh:
41
+ sa = json.load(fh)
42
+ now = int(time.time())
43
+ payload = {
44
+ "iss": sa["client_email"],
45
+ "scope": "https://www.googleapis.com/auth/androidpublisher",
46
+ "aud": "https://oauth2.googleapis.com/token",
47
+ "iat": now,
48
+ "exp": now + 3600,
49
+ }
50
+ signed = jwt.encode(payload, sa["private_key"], algorithm="RS256")
51
+ resp = requests.post(
52
+ "https://oauth2.googleapis.com/token",
53
+ data={
54
+ "grant_type": "urn:ietf:params:oauth:grant_type:jwt-bearer",
55
+ "assertion": signed,
56
+ },
57
+ timeout=TIMEOUT,
58
+ )
59
+ resp.raise_for_status()
60
+ return resp.json()["access_token"]
61
+
62
+
63
+ def check_readiness(package_name: str, access_token: str) -> dict:
64
+ """Check whether the Google Play app is ready for automated publishing."""
65
+ headers = {"Authorization": f"Bearer {access_token}"}
66
+ missing_steps: list[str] = []
67
+
68
+ # Create an edit session
69
+ edit_resp = requests.post(
70
+ f"{API_BASE}/{package_name}/edits",
71
+ headers={**headers, "Content-Type": "application/json"},
72
+ json={},
73
+ timeout=TIMEOUT,
74
+ )
75
+
76
+ if edit_resp.status_code == 404:
77
+ return {
78
+ "ready": False,
79
+ "missing_steps": ["1. CREATE APP: Go to Play Console > Create app"],
80
+ }
81
+
82
+ edit_resp.raise_for_status()
83
+ edit_id = edit_resp.json()["id"]
84
+
85
+ try:
86
+ # Check for uploaded bundles
87
+ bundles_resp = requests.get(
88
+ f"{API_BASE}/{package_name}/edits/{edit_id}/bundles",
89
+ headers=headers,
90
+ timeout=TIMEOUT,
91
+ )
92
+ bundles_resp.raise_for_status()
93
+ if not bundles_resp.json().get("bundles"):
94
+ missing_steps.append("2. UPLOAD FIRST AAB via Play Console")
95
+
96
+ # Check for track releases
97
+ tracks_resp = requests.get(
98
+ f"{API_BASE}/{package_name}/edits/{edit_id}/tracks",
99
+ headers=headers,
100
+ timeout=TIMEOUT,
101
+ )
102
+ tracks_resp.raise_for_status()
103
+ has_release = any(
104
+ release.get("versionCodes")
105
+ for track in tracks_resp.json().get("tracks", [])
106
+ for release in track.get("releases", [])
107
+ )
108
+ if not has_release:
109
+ missing_steps.append("3. COMPLETE SETUP: Content rating + pricing")
110
+ finally:
111
+ # Always clean up the edit
112
+ requests.delete(
113
+ f"{API_BASE}/{package_name}/edits/{edit_id}",
114
+ headers=headers,
115
+ timeout=TIMEOUT,
116
+ )
117
+
118
+ return {"ready": not missing_steps, "missing_steps": missing_steps}
119
+
120
+
121
+ def main() -> None:
122
+ sa_json = os.environ.get("SA_JSON", "")
123
+ package_name = os.environ.get("PACKAGE_NAME", "")
124
+
125
+ if not sa_json or not package_name:
126
+ print("ERROR: SA_JSON and PACKAGE_NAME env vars are required", file=sys.stderr)
127
+ sys.exit(1)
128
+
129
+ if not os.path.isfile(sa_json):
130
+ print(f"ERROR: Service account file not found: {sa_json}", file=sys.stderr)
131
+ sys.exit(1)
132
+
133
+ access_token = get_access_token(sa_json)
134
+ result = check_readiness(package_name, access_token)
135
+ print(json.dumps(result))
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()