@mpgd/cli 0.66.0 → 0.68.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.
@@ -275,7 +275,8 @@ async function submitVerifiedIosBuildResolved(input, run, { marketingVersion, bu
275
275
  let membershipFailure;
276
276
  try {
277
277
  const memberships = await run(['testflight', 'groups', 'list', '--build-id', build.id, '--internal'], lookupTimeoutMs);
278
- if (findInternalGroup(memberships, input.internalGroupId)) {
278
+ if (readBuildGroupMembership(memberships, build.id).some((group) => group.id === input.internalGroupId
279
+ && group.type === 'internal')) {
279
280
  return { ...base, status: 'testflight-ready', ...uploadReference, buildId: build.id };
280
281
  }
281
282
  }
@@ -405,6 +406,24 @@ function findInternalGroup(response, groupId) {
405
406
  && value.type === 'betaGroups' && value.id === groupId
406
407
  && isRecord(value.attributes) && value.attributes.isInternalGroup === true);
407
408
  }
409
+ /**
410
+ * asc 5.5.0 prints `testflight groups list --build-id` as its own membership
411
+ * result, not as App Store Connect resources:
412
+ * `{ buildId, appId, complete, groups: [{ id, name, type, membership }] }`
413
+ * (`internal/cli/testflight/build_group_membership.go`). An incomplete lookup
414
+ * is never read as "not a member".
415
+ */
416
+ export function readBuildGroupMembership(response, buildId) {
417
+ if (!isRecord(response) || response.buildId !== buildId || !Array.isArray(response.groups)
418
+ || response.groups.some((group) => !isRecord(group) || typeof group.id !== 'string'
419
+ || group.id === '' || typeof group.type !== 'string')) {
420
+ throw new Error('asc testflight groups list --build-id returned an invalid membership result.');
421
+ }
422
+ if (response.complete !== true) {
423
+ throw new Error('asc could not read every TestFlight group of the build.');
424
+ }
425
+ return response.groups;
426
+ }
408
427
  function readDataArray(response, command) {
409
428
  if (!isRecord(response) || !Array.isArray(response.data)) {
410
429
  throw new Error(`asc ${command} returned an invalid JSON schema.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -70,10 +70,10 @@
70
70
  "parse5": "7.3.0",
71
71
  "postcss": "8.5.28",
72
72
  "postcss-value-parser": "4.2.0",
73
- "@mpgd/catalog": "0.14.1",
73
+ "@mpgd/catalog": "0.15.0",
74
+ "@mpgd/target-config": "0.24.7",
74
75
  "@mpgd/phaser-assets": "0.9.1",
75
- "@mpgd/platform": "0.34.0",
76
- "@mpgd/target-config": "0.24.5"
76
+ "@mpgd/platform": "0.35.0"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@types/node": "^24.0.0",
@@ -81,19 +81,19 @@
81
81
  "@types/yauzl": "3.4.0",
82
82
  "ttsc": "0.34.2",
83
83
  "typescript": "7.0.2",
84
- "@mpgd/adapter-ait": "0.21.7",
85
- "@mpgd/adapter-capacitor": "0.18.0",
86
- "@mpgd/adapter-browser": "0.12.3",
87
- "@mpgd/adapter-devvit": "0.14.9",
88
- "@mpgd/adapter-verse8": "0.7.18",
89
- "@mpgd/bridge": "0.18.0",
90
- "@mpgd/capacitor-play-billing": "0.6.0",
91
- "@mpgd/analytics": "0.5.7",
92
- "@mpgd/capacitor-storekit": "0.6.0",
93
- "@mpgd/game-runtime": "0.9.0",
94
- "@mpgd/game-services": "0.32.0",
84
+ "@mpgd/adapter-ait": "0.21.9",
85
+ "@mpgd/adapter-browser": "0.12.4",
86
+ "@mpgd/adapter-capacitor": "0.19.1",
87
+ "@mpgd/adapter-devvit": "0.14.11",
88
+ "@mpgd/analytics": "0.5.8",
89
+ "@mpgd/bridge": "0.18.1",
90
+ "@mpgd/capacitor-play-billing": "0.6.2",
91
+ "@mpgd/adapter-verse8": "0.7.20",
92
+ "@mpgd/game-runtime": "0.10.1",
93
+ "@mpgd/capacitor-storekit": "0.7.1",
94
+ "@mpgd/game-services": "0.33.1",
95
95
  "@mpgd/lite-shell": "0.1.0",
96
- "@mpgd/i18n": "0.6.28"
96
+ "@mpgd/i18n": "0.6.29"
97
97
  },
98
98
  "main": "./dist/index.js",
99
99
  "types": "./dist/index.d.ts",
@@ -14,6 +14,9 @@ export type GameMessageKey =
14
14
  | 'reward'
15
15
  | 'rewardError'
16
16
  | 'rewardPending'
17
+ | 'rewardNotReady'
18
+ | 'rewardReconciling'
19
+ | 'rewardHistoryFull'
17
20
  | 'rewardUnavailable'
18
21
  | 'analytics'
19
22
  | 'bootError'
@@ -59,6 +62,9 @@ const messages = {
59
62
  reward: 'Rewarded ad smoke: {status}',
60
63
  rewardError: 'Rewarded ad smoke failed',
61
64
  rewardPending: 'Press R to request a rewarded ad smoke',
65
+ rewardNotReady: 'Rewarded ad smoke waits for the current purchase or ad',
66
+ rewardReconciling: 'Rewarded ad smoke waits until an earlier result is confirmed',
67
+ rewardHistoryFull: 'Rewarded ad smoke is unavailable until the game restarts',
62
68
  rewardUnavailable: 'Rewarded ads unavailable on this target',
63
69
  analytics: 'Analytics events: {count}',
64
70
  bootError: 'Failed to start game',
@@ -108,6 +114,9 @@ const messages = {
108
114
  reward: '리워드 광고 스모크: {status}',
109
115
  rewardError: '리워드 광고 스모크 실패',
110
116
  rewardPending: 'R 키로 리워드 광고 스모크 요청',
117
+ rewardNotReady: '진행 중인 구매나 광고가 끝나면 리워드 광고 스모크 요청 가능',
118
+ rewardReconciling: '이전 결과가 확인되면 리워드 광고 스모크 요청 가능',
119
+ rewardHistoryFull: '게임을 다시 시작할 때까지 리워드 광고 스모크를 사용할 수 없음',
111
120
  rewardUnavailable: '이 타깃에서는 리워드 광고를 사용할 수 없음',
112
121
  analytics: '애널리틱스 이벤트: {count}',
113
122
  bootError: '게임 시작 실패',
@@ -1,4 +1,4 @@
1
- import type { GameActionController } from '@mpgd/game-runtime/actions';
1
+ import type { GameActionAvailability, GameActionController } from '@mpgd/game-runtime/actions';
2
2
  import { bindPhaserGameScene } from '@mpgd/game-runtime/phaser';
3
3
  import Phaser from 'phaser';
4
4
 
@@ -21,6 +21,8 @@ export class PlayScene extends Phaser.Scene {
21
21
  private analyticsText!: Phaser.GameObjects.Text;
22
22
  private context!: StarterContext;
23
23
  private rewardedAction: GameActionController<'rewarded-ad'> | undefined;
24
+ private showingNotReady = false;
25
+ private requesting = false;
24
26
  private lastScore = -1;
25
27
  private lastAnalyticsCount = -1;
26
28
 
@@ -83,19 +85,67 @@ export class PlayScene extends Phaser.Scene {
83
85
  })
84
86
  .setOrigin(0.5);
85
87
 
86
- this.rewardedAction = this.context.gameRuntime.actions?.createRewardedAdController();
88
+ const actions = this.context.gameRuntime.actions;
89
+ this.rewardedAction = actions?.createRewardedAdController();
87
90
  const action = this.rewardedAction;
91
+ // Follow the runtime's availability, so the hint changes while any purchase
92
+ // (one from the shop included) or ad holds the screen, without a key press.
93
+ this.showingNotReady = false;
94
+ this.requesting = false;
95
+ const stopAvailability = actions?.subscribeAvailability((availability) => {
96
+ this.showAvailability(availability);
97
+ });
98
+ if (actions !== undefined) {
99
+ this.showAvailability(actions.getAvailability());
100
+ }
88
101
  const keyboard = this.input.keyboard;
89
102
  const onReward = () => {
90
103
  void this.requestRewardedAd();
91
104
  };
92
105
  keyboard?.on('keydown-R', onReward);
93
106
  this.events.once('shutdown', () => {
107
+ stopAvailability?.();
94
108
  action?.dispose();
95
109
  keyboard?.off('keydown-R', onReward);
96
110
  });
97
111
  }
98
112
 
113
+ private showAvailability(availability: GameActionAvailability): void {
114
+ // This scene's own request makes the actions busy too; its result text
115
+ // follows, so only someone else's purchase or ad shows the waiting text.
116
+ if (!this.rewardedAdsAvailable() || this.rewardedAction === undefined || availability === 'disposed'
117
+ || this.requesting) {
118
+ return;
119
+ }
120
+ if (availability === 'ready') {
121
+ // Restore the hint only over the waiting text, never over a result.
122
+ if (this.showingNotReady) {
123
+ this.showingNotReady = false;
124
+ this.rewardText.setText(this.rewardHint());
125
+ }
126
+ return;
127
+ }
128
+ this.showingNotReady = true;
129
+ // `history-full` lasts until the runtime is recreated; only `busy` ends
130
+ // with the other purchase or ad.
131
+ this.rewardText.setText(
132
+ t(
133
+ this.context.locale,
134
+ availability === 'history-full'
135
+ ? 'rewardHistoryFull'
136
+ : availability === 'reconciliation-required'
137
+ ? 'rewardReconciling'
138
+ : 'rewardNotReady',
139
+ ),
140
+ );
141
+ }
142
+
143
+ /** A result or error replaces the waiting text for good: `ready` must not restore the hint over it. */
144
+ private showRewardText(text: string): void {
145
+ this.showingNotReady = false;
146
+ this.rewardText.setText(text);
147
+ }
148
+
99
149
  override update(_time: number, delta: number): void {
100
150
  this.state = stepStarterRunState(this.state, delta);
101
151
 
@@ -139,13 +189,15 @@ export class PlayScene extends Phaser.Scene {
139
189
  const action = this.rewardedAction;
140
190
 
141
191
  if (!this.rewardedAdsAvailable() || action === undefined) {
142
- this.rewardText.setText(t(this.context.locale, 'rewardUnavailable'));
192
+ this.showRewardText(t(this.context.locale, 'rewardUnavailable'));
143
193
  return;
144
194
  }
145
195
 
196
+ // The tap-time guard stays: availability can change before the next frame.
146
197
  if (this.context.gameRuntime.actions?.getAvailability() !== 'ready') {
147
198
  return;
148
199
  }
200
+ this.requesting = true;
149
201
  try {
150
202
  const result = await action.execute({
151
203
  placementId: rewardedPlacementId,
@@ -155,7 +207,7 @@ export class PlayScene extends Phaser.Scene {
155
207
  if (action.isDisposed() || this.rewardedAction !== action) {
156
208
  return;
157
209
  }
158
- this.rewardText.setText(
210
+ this.showRewardText(
159
211
  result.claim?.granted === true
160
212
  ? t(this.context.locale, 'reward', { status: result.status })
161
213
  : t(this.context.locale, 'rewardError'),
@@ -169,9 +221,13 @@ export class PlayScene extends Phaser.Scene {
169
221
  },
170
222
  });
171
223
  if (!action.isDisposed() && this.rewardedAction === action) {
172
- this.rewardText.setText(t(this.context.locale, 'rewardError'));
224
+ this.showRewardText(t(this.context.locale, 'rewardError'));
173
225
  }
174
226
  console.error('[rewarded-ad]', error);
227
+ } finally {
228
+ if (this.rewardedAction === action) {
229
+ this.requesting = false;
230
+ }
175
231
  }
176
232
  }
177
233
  }