@daemux/store-automator 0.10.52 → 0.10.53
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 +2 -2
- package/package.json +1 -1
- package/plugins/store-automator/.claude-plugin/plugin.json +1 -1
- package/templates/github/workflows/ios-release.yml +3 -0
- package/templates/scripts/asc_subscription_submit.py +74 -0
- package/templates/scripts/ci/ios/ensure-app-exists.sh +50 -0
- package/templates/scripts/ci/ios/sync-iap.sh +1 -0
- package/templates/scripts/create_app_record.py +144 -65
- package/templates/scripts/sync_iap_ios.py +6 -0
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "App Store & Google Play automation for Flutter apps",
|
|
8
|
-
"version": "0.10.
|
|
8
|
+
"version": "0.10.53"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "store-automator",
|
|
13
13
|
"source": "./plugins/store-automator",
|
|
14
14
|
"description": "3 agents for app store publishing: reviewer, meta-creator, media-designer",
|
|
15
|
-
"version": "0.10.
|
|
15
|
+
"version": "0.10.53",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"flutter",
|
|
18
18
|
"app-store",
|
package/package.json
CHANGED
|
@@ -47,6 +47,9 @@ jobs:
|
|
|
47
47
|
- name: Install Python dependencies
|
|
48
48
|
run: pip3 install --break-system-packages requests PyJWT
|
|
49
49
|
|
|
50
|
+
- name: Ensure App Exists
|
|
51
|
+
run: scripts/ci/ios/ensure-app-exists.sh
|
|
52
|
+
|
|
50
53
|
- name: Upload Metadata & Screenshots
|
|
51
54
|
id: upload-metadata
|
|
52
55
|
run: scripts/ci/ios/upload-metadata.sh
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""App Store Connect Subscription Review Submission API layer.
|
|
2
|
+
|
|
3
|
+
Functions for submitting subscriptions and subscription groups for App Store review.
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import requests
|
|
9
|
+
except ImportError:
|
|
10
|
+
import subprocess
|
|
11
|
+
subprocess.check_call(
|
|
12
|
+
[sys.executable, "-m", "pip", "install", "--break-system-packages", "requests"],
|
|
13
|
+
stdout=subprocess.DEVNULL,
|
|
14
|
+
)
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
from asc_iap_api import BASE_URL, TIMEOUT, print_api_errors
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_review_submission(
|
|
21
|
+
headers: dict, sub_id: str, reviewer_notes: str = "",
|
|
22
|
+
) -> dict | None:
|
|
23
|
+
"""Submit a subscription for App Store review.
|
|
24
|
+
|
|
25
|
+
Idempotent: silently handles 409 Conflict (already submitted).
|
|
26
|
+
"""
|
|
27
|
+
resp = requests.post(
|
|
28
|
+
f"{BASE_URL}/subscriptionAppStoreReviewSubmissions",
|
|
29
|
+
json={"data": {
|
|
30
|
+
"type": "subscriptionAppStoreReviewSubmissions",
|
|
31
|
+
"attributes": {"reviewerNotes": reviewer_notes} if reviewer_notes else {},
|
|
32
|
+
"relationships": {
|
|
33
|
+
"subscription": {"data": {"type": "subscriptions", "id": sub_id}},
|
|
34
|
+
},
|
|
35
|
+
}},
|
|
36
|
+
headers=headers,
|
|
37
|
+
timeout=TIMEOUT,
|
|
38
|
+
)
|
|
39
|
+
if resp.status_code == 409:
|
|
40
|
+
return None
|
|
41
|
+
if not resp.ok:
|
|
42
|
+
print_api_errors(resp, f"submit subscription {sub_id} for review")
|
|
43
|
+
return None
|
|
44
|
+
print(f" Submitted subscription {sub_id} for review")
|
|
45
|
+
return resp.json().get("data")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def create_group_submission(
|
|
49
|
+
headers: dict, group_id: str,
|
|
50
|
+
) -> dict | None:
|
|
51
|
+
"""Submit a subscription group for App Store review.
|
|
52
|
+
|
|
53
|
+
Idempotent: silently handles 409 Conflict (already submitted).
|
|
54
|
+
"""
|
|
55
|
+
resp = requests.post(
|
|
56
|
+
f"{BASE_URL}/subscriptionGroupSubmissions",
|
|
57
|
+
json={"data": {
|
|
58
|
+
"type": "subscriptionGroupSubmissions",
|
|
59
|
+
"relationships": {
|
|
60
|
+
"subscriptionGroup": {
|
|
61
|
+
"data": {"type": "subscriptionGroups", "id": group_id},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
}},
|
|
65
|
+
headers=headers,
|
|
66
|
+
timeout=TIMEOUT,
|
|
67
|
+
)
|
|
68
|
+
if resp.status_code == 409:
|
|
69
|
+
return None
|
|
70
|
+
if not resp.ok:
|
|
71
|
+
print_api_errors(resp, f"submit group {group_id} for review")
|
|
72
|
+
return None
|
|
73
|
+
print(f" Submitted subscription group {group_id} for review")
|
|
74
|
+
return resp.json().get("data")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Ensures the app exists in App Store Connect (registers bundle ID, creates
|
|
3
|
+
# app record, sets content rights and pricing). Fully idempotent.
|
|
4
|
+
# Requires read-config.sh to have been sourced (provides PROJECT_ROOT, credentials, etc.).
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
source "$SCRIPT_DIR/../common/read-config.sh"
|
|
9
|
+
source "$SCRIPT_DIR/../common/ci-notify.sh"
|
|
10
|
+
|
|
11
|
+
echo "=== Ensure App Exists in App Store Connect ==="
|
|
12
|
+
|
|
13
|
+
# --- Validate ASC credentials ---
|
|
14
|
+
if [ -z "${APPLE_KEY_ID:-}" ] || [ -z "${APPLE_ISSUER_ID:-}" ]; then
|
|
15
|
+
ci_skip "ASC credentials not configured"
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if [ -z "${BUNDLE_ID:-}" ] || [ -z "${APP_NAME:-}" ]; then
|
|
19
|
+
ci_skip "BUNDLE_ID or APP_NAME not set in ci.config.yaml"
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# --- Set up App Store Connect API credentials ---
|
|
23
|
+
P8_FULL_PATH="$PROJECT_ROOT/$P8_KEY_PATH"
|
|
24
|
+
if [ ! -f "$P8_FULL_PATH" ]; then
|
|
25
|
+
echo "ERROR: P8 key file not found at $P8_FULL_PATH" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
export APP_STORE_CONNECT_KEY_IDENTIFIER="$APPLE_KEY_ID"
|
|
30
|
+
export APP_STORE_CONNECT_ISSUER_ID="$APPLE_ISSUER_ID"
|
|
31
|
+
export APP_STORE_CONNECT_PRIVATE_KEY="$(cat "$P8_FULL_PATH")"
|
|
32
|
+
export BUNDLE_ID="$BUNDLE_ID"
|
|
33
|
+
export APP_NAME="$APP_NAME"
|
|
34
|
+
export SKU="${SKU:-$BUNDLE_ID}"
|
|
35
|
+
export PLATFORM="${PLATFORM:-IOS}"
|
|
36
|
+
|
|
37
|
+
echo "ASC API key configured (Key ID: $APPLE_KEY_ID)"
|
|
38
|
+
|
|
39
|
+
# --- Run ensure-app-exists via Python ---
|
|
40
|
+
CREATE_SCRIPT="$PROJECT_ROOT/scripts/create_app_record.py"
|
|
41
|
+
|
|
42
|
+
if [ ! -f "$CREATE_SCRIPT" ]; then
|
|
43
|
+
echo "ERROR: create_app_record.py not found at $CREATE_SCRIPT" >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
echo "Ensuring app exists in App Store Connect..."
|
|
48
|
+
python3 "$CREATE_SCRIPT"
|
|
49
|
+
|
|
50
|
+
ci_done "App exists and is configured in App Store Connect"
|
|
@@ -20,6 +20,7 @@ fi
|
|
|
20
20
|
CURRENT_HASH=$(cat "$IAP_CONFIG" \
|
|
21
21
|
"$PROJECT_ROOT/scripts/sync_iap_ios.py" \
|
|
22
22
|
"$PROJECT_ROOT/scripts/asc_subscription_setup.py" \
|
|
23
|
+
"$PROJECT_ROOT/scripts/asc_subscription_submit.py" \
|
|
23
24
|
"$PROJECT_ROOT/scripts/asc_iap_api.py" \
|
|
24
25
|
| shasum -a 256 | cut -d' ' -f1)
|
|
25
26
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
Ensure an App Store Connect app exists with correct configuration.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Fully idempotent flow:
|
|
6
|
+
1. Register Bundle ID on the Developer Portal (skip if exists)
|
|
7
|
+
2. Create App Record in App Store Connect (skip if exists)
|
|
8
|
+
3. Set content rights declaration (via asc_app_setup)
|
|
9
|
+
4. Set app pricing schedule (via asc_app_setup)
|
|
7
10
|
|
|
8
11
|
Required env vars:
|
|
9
12
|
APP_STORE_CONNECT_KEY_IDENTIFIER - Key ID from App Store Connect
|
|
@@ -11,10 +14,15 @@ Required env vars:
|
|
|
11
14
|
APP_STORE_CONNECT_PRIVATE_KEY - Contents of the P8 key file
|
|
12
15
|
BUNDLE_ID - App bundle identifier (e.g. com.daemux.gigachat)
|
|
13
16
|
APP_NAME - Display name for the app
|
|
14
|
-
SKU - Unique SKU string
|
|
17
|
+
SKU - Unique SKU string (defaults to BUNDLE_ID)
|
|
18
|
+
|
|
19
|
+
Optional env vars:
|
|
20
|
+
PLATFORM - "IOS" or "UNIVERSAL" (default: "IOS")
|
|
21
|
+
CONTENT_RIGHTS_THIRD_PARTY - "true" if app uses third-party content (default: "false")
|
|
22
|
+
PRICE_TIER - Price tier integer (default: "0" for free)
|
|
15
23
|
|
|
16
24
|
Exit codes:
|
|
17
|
-
0 - App
|
|
25
|
+
0 - App ready (created or already existed)
|
|
18
26
|
1 - Any failure (missing env vars, API error, etc.)
|
|
19
27
|
"""
|
|
20
28
|
import json
|
|
@@ -35,6 +43,10 @@ except ImportError:
|
|
|
35
43
|
import jwt
|
|
36
44
|
import requests
|
|
37
45
|
|
|
46
|
+
# Import content rights and pricing from asc_app_setup (same directory)
|
|
47
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
48
|
+
from asc_app_setup import print_api_errors, set_content_rights, set_app_pricing # noqa: E402
|
|
49
|
+
|
|
38
50
|
BASE_URL = "https://api.appstoreconnect.apple.com/v1"
|
|
39
51
|
TIMEOUT = (10, 30)
|
|
40
52
|
|
|
@@ -48,13 +60,53 @@ def get_jwt_token(key_id: str, issuer_id: str, private_key: str) -> str:
|
|
|
48
60
|
"exp": now + 1200,
|
|
49
61
|
"aud": "appstoreconnect-v1",
|
|
50
62
|
}
|
|
51
|
-
return jwt.encode(
|
|
52
|
-
|
|
63
|
+
return jwt.encode(payload, private_key, algorithm="ES256", headers={"kid": key_id})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def ensure_bundle_id(headers: dict, bundle_id: str, app_name: str, platform: str) -> str:
|
|
67
|
+
"""Register a Bundle ID if it does not exist. Returns the resource ID."""
|
|
68
|
+
resp = requests.get(
|
|
69
|
+
f"{BASE_URL}/bundleIds",
|
|
70
|
+
params={"filter[identifier]": bundle_id},
|
|
71
|
+
headers=headers,
|
|
72
|
+
timeout=TIMEOUT,
|
|
53
73
|
)
|
|
74
|
+
resp.raise_for_status()
|
|
75
|
+
data = resp.json().get("data", [])
|
|
76
|
+
if data:
|
|
77
|
+
resource_id = data[0]["id"]
|
|
78
|
+
print(f" Bundle ID already registered: {resource_id}")
|
|
79
|
+
return resource_id
|
|
80
|
+
|
|
81
|
+
print(f" Registering new Bundle ID '{bundle_id}' (platform: {platform})...")
|
|
82
|
+
post_resp = requests.post(
|
|
83
|
+
f"{BASE_URL}/bundleIds",
|
|
84
|
+
json={
|
|
85
|
+
"data": {
|
|
86
|
+
"type": "bundleIds",
|
|
87
|
+
"attributes": {
|
|
88
|
+
"identifier": bundle_id,
|
|
89
|
+
"name": app_name,
|
|
90
|
+
"platform": platform,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
headers=headers,
|
|
95
|
+
timeout=TIMEOUT,
|
|
96
|
+
)
|
|
97
|
+
if post_resp.status_code == 409:
|
|
98
|
+
print(" Bundle ID already exists (409 Conflict), re-fetching...")
|
|
99
|
+
return _refetch_bundle_id(headers, bundle_id)
|
|
100
|
+
if not post_resp.ok:
|
|
101
|
+
print_api_errors(post_resp, "register bundle ID")
|
|
102
|
+
sys.exit(1)
|
|
103
|
+
resource_id = post_resp.json()["data"]["id"]
|
|
104
|
+
print(f" Bundle ID registered: {resource_id}")
|
|
105
|
+
return resource_id
|
|
54
106
|
|
|
55
107
|
|
|
56
|
-
def
|
|
57
|
-
"""
|
|
108
|
+
def _refetch_bundle_id(headers: dict, bundle_id: str) -> str:
|
|
109
|
+
"""Re-fetch a bundle ID resource after a 409 conflict."""
|
|
58
110
|
resp = requests.get(
|
|
59
111
|
f"{BASE_URL}/bundleIds",
|
|
60
112
|
params={"filter[identifier]": bundle_id},
|
|
@@ -64,15 +116,24 @@ def lookup_bundle_id_resource(headers: dict, bundle_id: str) -> str:
|
|
|
64
116
|
resp.raise_for_status()
|
|
65
117
|
data = resp.json().get("data", [])
|
|
66
118
|
if not data:
|
|
67
|
-
print(
|
|
68
|
-
f"ERROR: No Bundle ID found for identifier '{bundle_id}'. "
|
|
69
|
-
"Register it in App Store Connect > Certificates, Identifiers & Profiles first.",
|
|
70
|
-
file=sys.stderr,
|
|
71
|
-
)
|
|
119
|
+
print(f"ERROR: Bundle ID '{bundle_id}' not found after 409 conflict.", file=sys.stderr)
|
|
72
120
|
sys.exit(1)
|
|
73
121
|
return data[0]["id"]
|
|
74
122
|
|
|
75
123
|
|
|
124
|
+
def _lookup_existing_app(headers: dict, bundle_id: str) -> dict | None:
|
|
125
|
+
"""Look up an existing app by bundle ID. Returns app data dict or None."""
|
|
126
|
+
resp = requests.get(
|
|
127
|
+
f"{BASE_URL}/apps",
|
|
128
|
+
params={"filter[bundleId]": bundle_id},
|
|
129
|
+
headers=headers,
|
|
130
|
+
timeout=TIMEOUT,
|
|
131
|
+
)
|
|
132
|
+
resp.raise_for_status()
|
|
133
|
+
data = resp.json().get("data", [])
|
|
134
|
+
return data[0] if data else None
|
|
135
|
+
|
|
136
|
+
|
|
76
137
|
def create_app_record(
|
|
77
138
|
headers: dict,
|
|
78
139
|
bundle_id: str,
|
|
@@ -80,7 +141,18 @@ def create_app_record(
|
|
|
80
141
|
app_name: str,
|
|
81
142
|
sku: str,
|
|
82
143
|
) -> dict:
|
|
83
|
-
"""Create
|
|
144
|
+
"""Create or retrieve an existing app record in App Store Connect."""
|
|
145
|
+
existing = _lookup_existing_app(headers, bundle_id)
|
|
146
|
+
if existing:
|
|
147
|
+
app_info = {
|
|
148
|
+
"app_id": existing["id"],
|
|
149
|
+
"name": existing["attributes"]["name"],
|
|
150
|
+
"bundle_id": existing["attributes"]["bundleId"],
|
|
151
|
+
"sku": existing["attributes"]["sku"],
|
|
152
|
+
}
|
|
153
|
+
print(f" App already exists: {json.dumps(app_info)}")
|
|
154
|
+
return existing
|
|
155
|
+
|
|
84
156
|
payload = {
|
|
85
157
|
"data": {
|
|
86
158
|
"type": "apps",
|
|
@@ -92,80 +164,87 @@ def create_app_record(
|
|
|
92
164
|
},
|
|
93
165
|
"relationships": {
|
|
94
166
|
"bundleId": {
|
|
95
|
-
"data": {
|
|
96
|
-
"type": "bundleIds",
|
|
97
|
-
"id": bundle_id_resource_id,
|
|
98
|
-
}
|
|
167
|
+
"data": {"type": "bundleIds", "id": bundle_id_resource_id}
|
|
99
168
|
}
|
|
100
169
|
},
|
|
101
170
|
}
|
|
102
171
|
}
|
|
103
|
-
resp = requests.post(
|
|
104
|
-
f"{BASE_URL}/apps",
|
|
105
|
-
json=payload,
|
|
106
|
-
headers=headers,
|
|
107
|
-
timeout=TIMEOUT,
|
|
108
|
-
)
|
|
172
|
+
resp = requests.post(f"{BASE_URL}/apps", json=payload, headers=headers, timeout=TIMEOUT)
|
|
109
173
|
if resp.status_code == 409:
|
|
110
|
-
print(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
174
|
+
print(" App creation returned 409, fetching existing record...")
|
|
175
|
+
existing = _lookup_existing_app(headers, bundle_id)
|
|
176
|
+
if existing:
|
|
177
|
+
return existing
|
|
178
|
+
print("ERROR: App creation returned 409 but app not found.", file=sys.stderr)
|
|
114
179
|
sys.exit(1)
|
|
115
180
|
if not resp.ok:
|
|
116
|
-
|
|
117
|
-
for err in errors:
|
|
118
|
-
print(f"ERROR: {err.get('detail', err.get('title', 'Unknown'))}", file=sys.stderr)
|
|
181
|
+
print_api_errors(resp, "create app record")
|
|
119
182
|
sys.exit(1)
|
|
120
183
|
return resp.json()["data"]
|
|
121
184
|
|
|
122
185
|
|
|
123
|
-
def
|
|
186
|
+
def validate_env_vars() -> dict:
|
|
187
|
+
"""Validate and return all required/optional env vars as a dict."""
|
|
124
188
|
key_id = os.environ.get("APP_STORE_CONNECT_KEY_IDENTIFIER", "")
|
|
125
189
|
issuer_id = os.environ.get("APP_STORE_CONNECT_ISSUER_ID", "")
|
|
126
190
|
private_key = os.environ.get("APP_STORE_CONNECT_PRIVATE_KEY", "")
|
|
127
191
|
bundle_id = os.environ.get("BUNDLE_ID", "")
|
|
128
192
|
app_name = os.environ.get("APP_NAME", "")
|
|
129
|
-
sku = os.environ.get("SKU", "")
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if not
|
|
139
|
-
missing.append("BUNDLE_ID")
|
|
140
|
-
if not app_name:
|
|
141
|
-
missing.append("APP_NAME")
|
|
142
|
-
if not sku:
|
|
143
|
-
missing.append("SKU")
|
|
144
|
-
|
|
193
|
+
sku = os.environ.get("SKU", "") or bundle_id
|
|
194
|
+
|
|
195
|
+
required = {
|
|
196
|
+
"APP_STORE_CONNECT_KEY_IDENTIFIER": key_id,
|
|
197
|
+
"APP_STORE_CONNECT_ISSUER_ID": issuer_id,
|
|
198
|
+
"APP_STORE_CONNECT_PRIVATE_KEY": private_key,
|
|
199
|
+
"BUNDLE_ID": bundle_id,
|
|
200
|
+
"APP_NAME": app_name,
|
|
201
|
+
}
|
|
202
|
+
missing = [k for k, v in required.items() if not v]
|
|
145
203
|
if missing:
|
|
146
204
|
print(f"ERROR: Missing required environment variables: {', '.join(missing)}", file=sys.stderr)
|
|
147
205
|
sys.exit(1)
|
|
148
206
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
"
|
|
152
|
-
|
|
207
|
+
platform = os.environ.get("PLATFORM", "IOS")
|
|
208
|
+
if platform not in ("IOS", "UNIVERSAL"):
|
|
209
|
+
print(f"ERROR: PLATFORM must be 'IOS' or 'UNIVERSAL', got '{platform}'", file=sys.stderr)
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
|
|
212
|
+
price_tier_str = os.environ.get("PRICE_TIER", "0")
|
|
213
|
+
try:
|
|
214
|
+
price_tier = int(price_tier_str)
|
|
215
|
+
except ValueError:
|
|
216
|
+
print(f"ERROR: PRICE_TIER must be an integer, got '{price_tier_str}'", file=sys.stderr)
|
|
217
|
+
sys.exit(1)
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"key_id": key_id, "issuer_id": issuer_id, "private_key": private_key,
|
|
221
|
+
"bundle_id": bundle_id, "app_name": app_name, "sku": sku,
|
|
222
|
+
"platform": platform, "price_tier": price_tier,
|
|
223
|
+
"uses_third_party": os.environ.get("CONTENT_RIGHTS_THIRD_PARTY", "false").lower() == "true",
|
|
153
224
|
}
|
|
154
225
|
|
|
155
|
-
print(f"Looking up Bundle ID resource for '{bundle_id}'...")
|
|
156
|
-
bundle_id_resource_id = lookup_bundle_id_resource(headers, bundle_id)
|
|
157
|
-
print(f"Found Bundle ID resource: {bundle_id_resource_id}")
|
|
158
226
|
|
|
159
|
-
|
|
160
|
-
|
|
227
|
+
def main() -> None:
|
|
228
|
+
"""Orchestrate the full app creation and configuration flow."""
|
|
229
|
+
cfg = validate_env_vars()
|
|
230
|
+
token = get_jwt_token(cfg["key_id"], cfg["issuer_id"], cfg["private_key"])
|
|
231
|
+
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
161
232
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
print(f"App
|
|
233
|
+
print(f"Step 1: Ensuring Bundle ID '{cfg['bundle_id']}' is registered...")
|
|
234
|
+
bundle_id_resource_id = ensure_bundle_id(headers, cfg["bundle_id"], cfg["app_name"], cfg["platform"])
|
|
235
|
+
|
|
236
|
+
print(f"Step 2: Ensuring app record '{cfg['app_name']}' (SKU: {cfg['sku']}) exists...")
|
|
237
|
+
app_data = create_app_record(headers, cfg["bundle_id"], bundle_id_resource_id, cfg["app_name"], cfg["sku"])
|
|
238
|
+
app_id = app_data["id"]
|
|
239
|
+
print(f" App ID: {app_id}")
|
|
240
|
+
|
|
241
|
+
print("Step 3: Setting content rights declaration...")
|
|
242
|
+
set_content_rights(headers, app_id, cfg["uses_third_party"])
|
|
243
|
+
|
|
244
|
+
print("Step 4: Setting app pricing...")
|
|
245
|
+
set_app_pricing(headers, app_id, cfg["price_tier"])
|
|
246
|
+
|
|
247
|
+
print(f"All done. App '{cfg['app_name']}' ({cfg['bundle_id']}) is ready.")
|
|
169
248
|
|
|
170
249
|
|
|
171
250
|
if __name__ == "__main__":
|
|
@@ -52,6 +52,10 @@ from asc_subscription_setup import (
|
|
|
52
52
|
list_all_territory_ids,
|
|
53
53
|
upload_review_screenshot,
|
|
54
54
|
)
|
|
55
|
+
from asc_subscription_submit import (
|
|
56
|
+
create_group_submission,
|
|
57
|
+
create_review_submission,
|
|
58
|
+
)
|
|
55
59
|
|
|
56
60
|
CURRENCY_TO_TERRITORY = {
|
|
57
61
|
"USD": "USA", "EUR": "FRA", "GBP": "GBR", "JPY": "JPN",
|
|
@@ -155,8 +159,10 @@ def sync_subscription_group(
|
|
|
155
159
|
)
|
|
156
160
|
if not resp.ok:
|
|
157
161
|
print_api_errors(resp, f"touch subscription {sub_id}")
|
|
162
|
+
create_review_submission(headers, sub_id)
|
|
158
163
|
sub_results.append({"product_id": sub_config["product_id"], "id": sub_id})
|
|
159
164
|
|
|
165
|
+
create_group_submission(headers, group_id)
|
|
160
166
|
return {"group": ref_name, "group_id": group_id, "subscriptions": sub_results}
|
|
161
167
|
|
|
162
168
|
|