@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.
- package/.claude-plugin/marketplace.json +19 -0
- package/LICENSE +21 -0
- package/README.md +122 -0
- package/bin/cli.mjs +77 -0
- package/package.json +33 -0
- package/plugins/store-automator/.claude-plugin/plugin.json +9 -0
- package/plugins/store-automator/agents/appstore-media-designer.md +227 -0
- package/plugins/store-automator/agents/appstore-meta-creator.md +185 -0
- package/plugins/store-automator/agents/appstore-reviewer.md +180 -0
- package/src/dependency-check.mjs +26 -0
- package/src/install.mjs +140 -0
- package/src/mcp-setup.mjs +93 -0
- package/src/prompt.mjs +55 -0
- package/src/settings.mjs +106 -0
- package/src/templates.mjs +55 -0
- package/src/uninstall.mjs +100 -0
- package/src/utils.mjs +46 -0
- package/templates/CLAUDE.md.template +219 -0
- package/templates/Gemfile.template +2 -0
- package/templates/ci.config.yaml.template +51 -0
- package/templates/codemagic.template.yaml +289 -0
- package/templates/fastlane/android/Appfile.template +2 -0
- package/templates/fastlane/android/Fastfile.template +36 -0
- package/templates/fastlane/android/Pluginfile.template +1 -0
- package/templates/fastlane/app_rating_config.json.template +17 -0
- package/templates/fastlane/iap_config.json.template +53 -0
- package/templates/fastlane/ios/Appfile.template +2 -0
- package/templates/fastlane/ios/Deliverfile.template +1 -0
- package/templates/fastlane/ios/Fastfile.template +47 -0
- package/templates/fastlane/ios/Pluginfile.template +1 -0
- package/templates/fastlane/ios/Snapfile.template +26 -0
- package/templates/scripts/check_changed.sh +23 -0
- package/templates/scripts/check_google_play.py +139 -0
- package/templates/scripts/generate.sh +77 -0
- package/templates/scripts/manage_version_ios.py +168 -0
- package/templates/web/deploy-cloudflare.mjs +240 -0
- package/templates/web/marketing.html +121 -0
- package/templates/web/privacy.html +119 -0
- package/templates/web/styles.css +377 -0
- package/templates/web/support.html +156 -0
- 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,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 @@
|
|
|
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()
|