@mpgd/cli 0.56.0 → 0.58.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/dist/capacitor-shell-starter.d.ts +30 -0
- package/dist/capacitor-shell-starter.js +243 -11
- package/dist/index.js +170 -11
- package/dist/legal-site-output.d.ts +18 -0
- package/dist/legal-site-output.js +46 -1
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +3935 -1308
- package/dist/native-deploy-run.js +4 -0
- package/dist/native-shell-store-policy.d.ts +123 -0
- package/dist/native-shell-store-policy.js +816 -0
- package/dist/notifications-starter.js +26 -3
- package/dist/release-state.d.ts +2 -0
- package/dist/release-state.js +4 -2
- package/dist/testflight-submission.d.ts +9 -0
- package/dist/testflight-submission.js +69 -0
- package/package.json +16 -16
- package/templates/lite-overlay/agent/acceptance.md +1 -1
- package/templates/lite-overlay/agent/brief.md +1 -1
- package/templates/lite-overlay/src/lite/deleteAccountButton.ts +173 -0
- package/templates/lite-overlay/src/lite/restorePurchasesButton.ts +63 -0
- package/templates/lite-overlay/src/runtime/createGame.ts +8 -0
- package/templates/notifications-overlay/common/docs/NOTIFICATIONS.md +28 -6
- package/templates/notifications-overlay/common/notifications/deployment.json +2 -1
- package/templates/notifications-overlay/common/notifications/topics.json +3 -0
- package/templates/notifications-overlay/common/src/platform/capacitorNotifications.ts +8 -3
- package/templates/notifications-overlay/common/src/platform/notificationBackend.ts +111 -2
- package/templates/notifications-overlay/common/src/platform/notifications.ts +36 -4
- package/templates/phaser-game/README.md +72 -0
- package/templates/phaser-game/agent/acceptance.md +4 -1
- package/templates/phaser-game/agent/brief.md +14 -4
- package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +37 -0
- package/templates/phaser-game/gitignore +11 -0
- package/templates/phaser-game/legal/delete-account.html +133 -0
- package/templates/phaser-game/legal/privacy.html +111 -12
- package/templates/phaser-game/legal/support.html +1 -0
- package/templates/phaser-game/legal/terms.html +1 -0
- package/templates/phaser-game/mpgd.game.json +3 -0
- package/templates/phaser-game/src/env.d.ts +6 -0
- package/templates/phaser-game/src/i18n/messages.ts +86 -1
- package/templates/phaser-game/src/main.ts +69 -8
- package/templates/phaser-game/src/platform/commerceAccountBinding.ts +39 -10
- package/templates/phaser-game/src/platform/deleteAccount.ts +334 -0
- package/templates/phaser-game/src/platform/gameServices.ts +55 -10
- package/templates/phaser-game/src/platform/guestIdentity.ts +301 -0
- package/templates/phaser-game/src/platform/restorePurchases.ts +205 -0
- package/templates/phaser-game/src/platform/serverSession.ts +8 -0
- package/templates/phaser-game/src/platform/sessionGatedServices.ts +217 -0
- package/templates/phaser-game/src/scenes/LobbyScene.ts +254 -1
- package/templates/phaser-game/src/scenes/PlayScene.ts +16 -6
|
@@ -48,30 +48,129 @@
|
|
|
48
48
|
<a href="/privacy/" aria-current="page">Privacy</a>
|
|
49
49
|
<a href="/support/">Support</a>
|
|
50
50
|
<a href="/terms/">Terms</a>
|
|
51
|
+
<a href="/delete-account/">Delete account</a>
|
|
51
52
|
</nav>
|
|
52
53
|
<h1>__GAME_TITLE__ Privacy Policy</h1>
|
|
53
54
|
<p>Last updated: __LEGAL_LAST_UPDATED__</p>
|
|
54
55
|
<p>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
This policy explains what __GAME_TITLE__ (the "game") and its operator,
|
|
57
|
+
[REPLACE: legal name of the developer or company] ("we"), collect, why,
|
|
58
|
+
how long we keep it, and how to have it deleted.
|
|
58
59
|
</p>
|
|
60
|
+
<!--
|
|
61
|
+
Starting point, not legal advice. Before publishing, replace every
|
|
62
|
+
REPLACE marker in square brackets (mpgd legal build warns while any remain) and
|
|
63
|
+
delete what the game does not do or use.
|
|
64
|
+
-->
|
|
59
65
|
<h2>Information We Process</h2>
|
|
66
|
+
<ul>
|
|
67
|
+
<li>
|
|
68
|
+
<strong>Player identifiers.</strong> A random guest player ID issued by
|
|
69
|
+
our game server, a random ID for this installation of the game, and,
|
|
70
|
+
when you sign in, the account ID of the sign-in provider. Store purchases are bound to a one-way hash of your signed-in player ID.
|
|
71
|
+
</li>
|
|
72
|
+
<li>
|
|
73
|
+
<strong>Game progress.</strong> Saves, settings, scores, and the
|
|
74
|
+
leaderboard entries you submit.
|
|
75
|
+
</li>
|
|
76
|
+
<li>
|
|
77
|
+
<strong>Purchases.</strong> The products you buy and the store's
|
|
78
|
+
transaction records (Google Play order IDs and a one-way hash of the
|
|
79
|
+
purchase token, which our server sends to Google to verify the
|
|
80
|
+
purchase; App Store transaction and original transaction IDs), kept in
|
|
81
|
+
our purchase ledger so a purchase is granted once and can be
|
|
82
|
+
restored. We never receive your payment card details.
|
|
83
|
+
</li>
|
|
84
|
+
<li>
|
|
85
|
+
<strong>Rewarded ads.</strong> Records of ads you chose to watch for a
|
|
86
|
+
reward, so the reward is granted once.
|
|
87
|
+
</li>
|
|
88
|
+
<li>
|
|
89
|
+
<strong>Notifications.</strong> If you allow notifications, a push
|
|
90
|
+
token for your device and your notification choices, including any
|
|
91
|
+
consent to advertising messages and when you gave it.
|
|
92
|
+
</li>
|
|
93
|
+
<li>
|
|
94
|
+
<strong>Technical data.</strong> App version, platform, coarse device
|
|
95
|
+
type, and error details needed to operate and fix the game.
|
|
96
|
+
</li>
|
|
97
|
+
<li><strong>Support.</strong> Messages you send us.</li>
|
|
98
|
+
</ul>
|
|
99
|
+
<h2>How We Use Information</h2>
|
|
60
100
|
<p>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
101
|
+
To run the game and its online features, keep your progress and
|
|
102
|
+
purchases, grant purchases and rewards exactly once, prevent fraud and
|
|
103
|
+
cheating, send the notifications you asked for, answer support requests,
|
|
104
|
+
and meet store and legal requirements. We do not sell your personal
|
|
105
|
+
information. [REPLACE: keep "We do not track you across other
|
|
106
|
+
companies' apps or websites." only if the game shows no personalized
|
|
107
|
+
ads and uses no other tracking; otherwise describe the tracking and how
|
|
108
|
+
to refuse it.]
|
|
64
109
|
</p>
|
|
65
|
-
<h2>
|
|
110
|
+
<h2>Third-Party Services</h2>
|
|
111
|
+
<!-- Keep only the services the game uses. -->
|
|
112
|
+
<p>
|
|
113
|
+
These third-party services are involved in running the game; some act on
|
|
114
|
+
our behalf and some under their own privacy policies.
|
|
115
|
+
</p>
|
|
116
|
+
<ul>
|
|
117
|
+
<li>
|
|
118
|
+
Google Play Billing and the Google Play Developer API (Google) or Apple
|
|
119
|
+
StoreKit and the App Store Server API (Apple): purchases and their
|
|
120
|
+
verification.
|
|
121
|
+
</li>
|
|
122
|
+
<li>
|
|
123
|
+
Firebase Cloud Messaging (Google): delivering push notifications.
|
|
124
|
+
</li>
|
|
125
|
+
<li>
|
|
126
|
+
Google AdMob and the User Messaging Platform (Google): showing ads and
|
|
127
|
+
asking for consent where required. AdMob may use the device
|
|
128
|
+
advertising identifier where the platform and your choices allow it.
|
|
129
|
+
</li>
|
|
130
|
+
<li>
|
|
131
|
+
Cloudflare: hosting our game server, database, and these pages.
|
|
132
|
+
</li>
|
|
133
|
+
<li>[REPLACE: any other analytics, crash-reporting, or sign-in service.]</li>
|
|
134
|
+
</ul>
|
|
135
|
+
<h2>Retention</h2>
|
|
136
|
+
<p>
|
|
137
|
+
We keep account, progress, and notification data while your account is
|
|
138
|
+
active and delete it when you delete your account [REPLACE: and/or
|
|
139
|
+
after N months of inactivity]. We keep purchase records (product,
|
|
140
|
+
store order or transaction number, and date) for 5 years from the
|
|
141
|
+
purchase or its last refund, whichever is later, for payment and supply
|
|
142
|
+
record obligations
|
|
143
|
+
[REPLACE: the laws that apply to you, for example Korea's Act on
|
|
144
|
+
Consumer Protection in Electronic Commerce], also after the rest of
|
|
145
|
+
your data is deleted, stored separately and without your player ID. Server logs are kept for
|
|
146
|
+
[REPLACE: period].
|
|
147
|
+
</p>
|
|
148
|
+
<h2 id="delete-account">Deleting Your Account and Data</h2>
|
|
149
|
+
<p>
|
|
150
|
+
You can delete your account and the data linked to it in the game
|
|
151
|
+
([REPLACE: menu path, for example the lobby's Delete account button])
|
|
152
|
+
or, without reinstalling the game, by following the steps on our
|
|
153
|
+
<a href="/delete-account/">account deletion page</a>. The deletion
|
|
154
|
+
page lists what is deleted and what we must keep (see Retention).
|
|
155
|
+
</p>
|
|
156
|
+
<h2>Your Choices</h2>
|
|
157
|
+
<p>
|
|
158
|
+
You can turn notifications off in the device settings [REPLACE: and in
|
|
159
|
+
the game's settings, if it has a notification settings screen], change
|
|
160
|
+
your ad privacy choices [REPLACE: from the game's privacy options, if it
|
|
161
|
+
shows ads with a consent form], and ask us for a copy or correction of
|
|
162
|
+
your data. [REPLACE: rights and contact points required where you
|
|
163
|
+
operate, for example GDPR, UK GDPR, CCPA, or Korea's PIPA.]
|
|
164
|
+
</p>
|
|
165
|
+
<h2>Children</h2>
|
|
66
166
|
<p>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
review or safety requirements.
|
|
167
|
+
[REPLACE: who the game is for. If it is directed at children, describe
|
|
168
|
+
the stricter handling the store programs require.]
|
|
70
169
|
</p>
|
|
71
170
|
<h2>Contact</h2>
|
|
72
171
|
<p>
|
|
73
|
-
|
|
74
|
-
contact
|
|
172
|
+
[REPLACE: support email or form, postal address, and data protection
|
|
173
|
+
contact if required.]
|
|
75
174
|
</p>
|
|
76
175
|
</main>
|
|
77
176
|
</body>
|
|
@@ -19,6 +19,12 @@ interface ImportMetaEnv {
|
|
|
19
19
|
readonly VITE_MPGD_GAME_SERVICES_URL?: string;
|
|
20
20
|
readonly VITE_MPGD_GAME_SERVICES_TARGET?: string;
|
|
21
21
|
readonly VITE_MPGD_GAME_SERVICES_TRANSPORT?: string;
|
|
22
|
+
/**
|
|
23
|
+
* The guest identity service (the kit's reference Worker,
|
|
24
|
+
* `apps/game-identity-worker`): an https URL, or http on a local
|
|
25
|
+
* development host. See `src/platform/guestIdentity.ts`.
|
|
26
|
+
*/
|
|
27
|
+
readonly VITE_MPGD_IDENTITY_URL?: string;
|
|
22
28
|
/**
|
|
23
29
|
* Local development only: `<playerId>` or `<playerId>.<installationId>`
|
|
24
30
|
* for an unauthenticated notifications session (see `serverSession.ts`).
|
|
@@ -16,7 +16,32 @@ export type GameMessageKey =
|
|
|
16
16
|
| 'rewardPending'
|
|
17
17
|
| 'rewardUnavailable'
|
|
18
18
|
| 'analytics'
|
|
19
|
-
| 'bootError'
|
|
19
|
+
| 'bootError'
|
|
20
|
+
| 'restorePurchases'
|
|
21
|
+
| 'restoreWorking'
|
|
22
|
+
| 'restoreDone'
|
|
23
|
+
| 'restoreToDevice'
|
|
24
|
+
| 'restoreHolderLimit'
|
|
25
|
+
| 'restoreNothing'
|
|
26
|
+
| 'restoreIncomplete'
|
|
27
|
+
| 'restorePartial'
|
|
28
|
+
| 'restoreNeedsSession'
|
|
29
|
+
| 'restoreNeedsConnection'
|
|
30
|
+
| 'restoreFailed'
|
|
31
|
+
| 'deleteAccount'
|
|
32
|
+
| 'deleteAccountConfirm'
|
|
33
|
+
| 'deleteAccountPendingPurchase'
|
|
34
|
+
| 'deleteAccountConfirmButton'
|
|
35
|
+
| 'deleteAccountCancel'
|
|
36
|
+
| 'deleteAccountClose'
|
|
37
|
+
| 'deleteAccountWorking'
|
|
38
|
+
| 'deleteAccountDone'
|
|
39
|
+
| 'deleteAccountDoneLocalFailed'
|
|
40
|
+
| 'deleteAccountSessionEnded'
|
|
41
|
+
| 'deleteAccountNeedsSession'
|
|
42
|
+
| 'deleteAccountUnavailable'
|
|
43
|
+
| 'deleteAccountFailed'
|
|
44
|
+
| 'deleteAccountRejected';
|
|
20
45
|
|
|
21
46
|
const messages = {
|
|
22
47
|
en: {
|
|
@@ -36,6 +61,36 @@ const messages = {
|
|
|
36
61
|
rewardUnavailable: 'Rewarded ads unavailable on this target',
|
|
37
62
|
analytics: 'Analytics events: {count}',
|
|
38
63
|
bootError: 'Failed to start game',
|
|
64
|
+
restorePurchases: 'Restore purchases',
|
|
65
|
+
restoreWorking: 'Restoring purchases...',
|
|
66
|
+
restoreDone: 'Purchases restored ({count} confirmed)',
|
|
67
|
+
restoreToDevice: 'Restored to this device ({count} confirmed)',
|
|
68
|
+
restoreHolderLimit: 'This purchase is already in use on the maximum number of devices',
|
|
69
|
+
restoreNothing: 'No new purchases to restore',
|
|
70
|
+
restoreIncomplete: 'Some purchases are not confirmed yet. Try again later.',
|
|
71
|
+
restorePartial: 'Purchases restored ({count}); others are not confirmed yet. Try again later.',
|
|
72
|
+
restoreNeedsSession: 'Restoring purchases needs a game server session',
|
|
73
|
+
restoreNeedsConnection: 'Could not reach the game server. Tap to try again.',
|
|
74
|
+
restoreFailed: 'Could not restore purchases. Try again later.',
|
|
75
|
+
deleteAccount: 'Delete account',
|
|
76
|
+
deleteAccountConfirm:
|
|
77
|
+
'Delete your account? Your progress, scores, and notification settings are erased and you are signed out. '
|
|
78
|
+
+ 'Purchases are not refunded; ask the store you bought from for a refund. Items you bought once (such as Remove ads) can be restored to a new account with Restore purchases while the store account still owns them. '
|
|
79
|
+
+ 'Purchase records are kept for 5 years from the purchase or its last refund, for payment and supply record obligations, without your player ID.',
|
|
80
|
+
deleteAccountPendingPurchase:
|
|
81
|
+
'A purchase is still being processed. Deleting now loses it: Google Play refunds it, but for an App Store purchase ask Apple for the refund.',
|
|
82
|
+
deleteAccountConfirmButton: 'Delete permanently',
|
|
83
|
+
deleteAccountCancel: 'Cancel',
|
|
84
|
+
deleteAccountClose: 'Close',
|
|
85
|
+
deleteAccountWorking: 'Deleting your account...',
|
|
86
|
+
deleteAccountDone: 'Your account was deleted and you are signed out.',
|
|
87
|
+
deleteAccountDoneLocalFailed:
|
|
88
|
+
'Your account was deleted, but this device could not forget its sign-in. Clear the app data or reinstall the app.',
|
|
89
|
+
deleteAccountSessionEnded: 'Your session has ended: your account deletion most likely completed.',
|
|
90
|
+
deleteAccountNeedsSession: 'Sign in to delete your account.',
|
|
91
|
+
deleteAccountUnavailable: 'Account deletion is not available here. Use the deletion page linked from the privacy policy.',
|
|
92
|
+
deleteAccountFailed: 'Could not delete your account. Try again.',
|
|
93
|
+
deleteAccountRejected: 'Could not delete your account. Contact support.',
|
|
39
94
|
},
|
|
40
95
|
ko: {
|
|
41
96
|
title: '__GAME_TITLE__',
|
|
@@ -54,6 +109,36 @@ const messages = {
|
|
|
54
109
|
rewardUnavailable: '이 타깃에서는 리워드 광고를 사용할 수 없음',
|
|
55
110
|
analytics: '애널리틱스 이벤트: {count}',
|
|
56
111
|
bootError: '게임 시작 실패',
|
|
112
|
+
restorePurchases: '구매 복원',
|
|
113
|
+
restoreWorking: '구매 복원 중...',
|
|
114
|
+
restoreDone: '구매 복원 완료 ({count}건 확인)',
|
|
115
|
+
restoreToDevice: '이 기기로 구매를 복원했습니다 ({count}건 확인)',
|
|
116
|
+
restoreHolderLimit: '이 구매는 이미 허용된 최대 기기 수에서 사용 중입니다',
|
|
117
|
+
restoreNothing: '새로 복원할 구매가 없습니다',
|
|
118
|
+
restoreIncomplete: '아직 확인되지 않은 구매가 있습니다. 나중에 다시 시도하세요.',
|
|
119
|
+
restorePartial: '{count}건 복원, 아직 확인되지 않은 구매가 있습니다. 나중에 다시 시도하세요.',
|
|
120
|
+
restoreNeedsSession: '구매를 복원하려면 게임 서버 세션이 필요합니다',
|
|
121
|
+
restoreNeedsConnection: '게임 서버에 연결하지 못했습니다. 다시 시도하려면 누르세요.',
|
|
122
|
+
restoreFailed: '구매를 복원하지 못했습니다. 나중에 다시 시도하세요.',
|
|
123
|
+
deleteAccount: '계정 삭제',
|
|
124
|
+
deleteAccountConfirm:
|
|
125
|
+
'계정을 삭제할까요? 진행 상황, 점수, 알림 설정이 지워지고 로그아웃됩니다. '
|
|
126
|
+
+ '구매는 환불되지 않습니다. 환불은 구매한 스토어에 요청하세요. 한 번 구매한 항목(광고 제거 등)은 스토어 계정이 계속 보유하고 있으면 구매 복원으로 새 계정에서 다시 받을 수 있습니다. '
|
|
127
|
+
+ '구매 기록은 거래기록 보존 의무에 따라 구매 또는 마지막 환불 시점부터 5년 동안 플레이어 ID 없이 보관됩니다.',
|
|
128
|
+
deleteAccountPendingPurchase:
|
|
129
|
+
'아직 처리 중인 구매가 있습니다. 지금 삭제하면 이 구매는 지급되지 않습니다. Google Play는 자동으로 환불하지만, App Store 구매는 Apple에 환불을 요청해야 합니다.',
|
|
130
|
+
deleteAccountConfirmButton: '영구 삭제',
|
|
131
|
+
deleteAccountCancel: '취소',
|
|
132
|
+
deleteAccountClose: '닫기',
|
|
133
|
+
deleteAccountWorking: '계정을 삭제하는 중...',
|
|
134
|
+
deleteAccountDone: '계정이 삭제되었고 로그아웃되었습니다.',
|
|
135
|
+
deleteAccountDoneLocalFailed:
|
|
136
|
+
'계정은 삭제되었지만 이 기기의 로그인 정보를 지우지 못했습니다. 앱 데이터를 지우거나 앱을 다시 설치하세요.',
|
|
137
|
+
deleteAccountSessionEnded: '세션이 종료되었습니다. 계정 삭제가 완료되었을 가능성이 높습니다.',
|
|
138
|
+
deleteAccountNeedsSession: '계정을 삭제하려면 로그인하세요.',
|
|
139
|
+
deleteAccountUnavailable: '여기서는 계정을 삭제할 수 없습니다. 개인정보처리방침에 연결된 삭제 페이지를 이용하세요.',
|
|
140
|
+
deleteAccountFailed: '계정을 삭제하지 못했습니다. 다시 시도하세요.',
|
|
141
|
+
deleteAccountRejected: '계정을 삭제하지 못했습니다. 고객 지원에 문의하세요.',
|
|
57
142
|
},
|
|
58
143
|
} satisfies Record<Locale, Record<GameMessageKey, string>>;
|
|
59
144
|
|
|
@@ -17,7 +17,13 @@ import { t } from './i18n/messages';
|
|
|
17
17
|
import { createClientId } from './runtime/id';
|
|
18
18
|
import { createStarterGame } from './runtime/createGame';
|
|
19
19
|
import { detectRuntime } from './platform/runtimeDetector';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createStarterGameServices,
|
|
22
|
+
createStarterSessionGatedGameServices,
|
|
23
|
+
type StarterMonetizationPorts,
|
|
24
|
+
} from './platform/gameServices';
|
|
25
|
+
import { createStarterGuestSignIn } from './platform/guestIdentity';
|
|
26
|
+
import { setStarterServerSession } from './platform/serverSession';
|
|
21
27
|
import { installPlatform } from './platform/installPlatform';
|
|
22
28
|
import { installMicrosoftStorePwa } from './platform/microsoftStorePwa';
|
|
23
29
|
|
|
@@ -28,6 +34,7 @@ async function bootstrap(options: { readonly monetization?: StarterMonetizationP
|
|
|
28
34
|
let gameRuntime: GamePlatformRuntime | undefined;
|
|
29
35
|
let locale: Locale = 'en';
|
|
30
36
|
let disposeMicrosoftStorePwa: (() => void) | undefined;
|
|
37
|
+
let disposeGuestSignIn: (() => void) | undefined;
|
|
31
38
|
|
|
32
39
|
try {
|
|
33
40
|
if (__APP_TARGET__ === 'ait') {
|
|
@@ -53,15 +60,45 @@ async function bootstrap(options: { readonly monetization?: StarterMonetizationP
|
|
|
53
60
|
runtime: runtime.config.runtime,
|
|
54
61
|
orientationPolicy,
|
|
55
62
|
});
|
|
56
|
-
const
|
|
63
|
+
const platformPlayer =
|
|
57
64
|
(await platform.identity.getPlayer()) ?? {
|
|
58
65
|
playerId: 'local-player',
|
|
59
66
|
displayName: 'Local Player',
|
|
60
67
|
};
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
// The server-issued guest (VITE_MPGD_IDENTITY_URL): tried at boot, and when that fails,
|
|
69
|
+
// again in the background and before any purchase or restore (`guestIdentity.ts`).
|
|
70
|
+
const guestSignIn = createStarterGuestSignIn({
|
|
71
|
+
identityUrl: import.meta.env.VITE_MPGD_IDENTITY_URL,
|
|
72
|
+
credentials: platform.secureCredentials,
|
|
73
|
+
});
|
|
74
|
+
disposeGuestSignIn = () => guestSignIn.dispose();
|
|
75
|
+
const [bootSignIn, platformSession, launchIntent] = await Promise.all([
|
|
76
|
+
guestSignIn.ensure(),
|
|
77
|
+
resolveIdentitySession(platform, platformPlayer.playerId),
|
|
63
78
|
resolveLaunchIntent(platform),
|
|
64
79
|
]);
|
|
80
|
+
// A background retry may have won while the other boot reads were pending.
|
|
81
|
+
const guestIdentity = guestSignIn.current() ?? bootSignIn;
|
|
82
|
+
// Before the game services read the session: at once, or when a later attempt succeeds
|
|
83
|
+
// (notifications then come up on their next open; the session-gated services read it).
|
|
84
|
+
if (guestIdentity !== undefined) {
|
|
85
|
+
setStarterServerSession(guestIdentity.serverSession);
|
|
86
|
+
} else {
|
|
87
|
+
guestSignIn.onSignedIn((later) => setStarterServerSession(later.serverSession));
|
|
88
|
+
}
|
|
89
|
+
// Signed in at boot, the server player replaces the platform's (on the native shell a local
|
|
90
|
+
// placeholder) everywhere. Signed in later, the scenes keep this run's boot player (see
|
|
91
|
+
// README, "Guest identity"); only the purchase and notification services use the server player.
|
|
92
|
+
const player = guestIdentity !== undefined
|
|
93
|
+
? { ...platformPlayer, playerId: guestIdentity.playerId }
|
|
94
|
+
: platformPlayer;
|
|
95
|
+
const identitySession: IdentitySession = guestIdentity !== undefined
|
|
96
|
+
? {
|
|
97
|
+
identityLevel: guestIdentity.identityLevel,
|
|
98
|
+
playerId: guestIdentity.playerId,
|
|
99
|
+
trustLevel: 'server-verified',
|
|
100
|
+
}
|
|
101
|
+
: platformSession;
|
|
65
102
|
locale = resolveTargetMpgdLocale({
|
|
66
103
|
capabilities: runtime.capabilities,
|
|
67
104
|
...(platform.gameSettings?.getLocale === undefined ? {} : { preferredLocales: [platform.gameSettings.getLocale()] }),
|
|
@@ -76,10 +113,16 @@ async function bootstrap(options: { readonly monetization?: StarterMonetizationP
|
|
|
76
113
|
sessionId: analyticsSessionId,
|
|
77
114
|
sink: analyticsSink,
|
|
78
115
|
});
|
|
116
|
+
const sessionGated = guestIdentity === undefined && guestSignIn.configured;
|
|
79
117
|
const ownedRuntime = createGamePlatformRuntime({
|
|
80
118
|
gateway: platform,
|
|
81
119
|
initialLifecycleState: document.visibilityState === 'hidden' ? 'inactive' : 'active',
|
|
82
120
|
canShow: (placement) => {
|
|
121
|
+
// A run that booted before its sign-in shows no rewarded ad: the runtime's late
|
|
122
|
+
// reward evidence recovery is bound at boot, which had no recovery client.
|
|
123
|
+
if (placement.format === 'rewarded' && sessionGated) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
83
126
|
const enabled = placement.format === 'rewarded'
|
|
84
127
|
? runtime.config.features.rewardedAds
|
|
85
128
|
: runtime.config.features.interstitialAds;
|
|
@@ -100,15 +143,32 @@ async function bootstrap(options: { readonly monetization?: StarterMonetizationP
|
|
|
100
143
|
onObserverError: (error) => {
|
|
101
144
|
console.error('[game-runtime]', error);
|
|
102
145
|
},
|
|
103
|
-
createServices: (gateway) =>
|
|
104
|
-
|
|
146
|
+
createServices: (gateway) => {
|
|
147
|
+
const servicesInput = {
|
|
105
148
|
gateway,
|
|
106
|
-
playerId: identitySession.playerId ?? player.playerId,
|
|
107
149
|
configTarget: runtime.configTarget,
|
|
108
150
|
...(options.monetization === undefined ? {} : { operationStore: options.monetization.operationStore }),
|
|
109
151
|
analytics: analyticsSink,
|
|
110
152
|
analyticsSessionId,
|
|
111
|
-
}
|
|
153
|
+
};
|
|
154
|
+
// Configured but not signed in yet: the services wait for the server player, never
|
|
155
|
+
// binding purchases or the journal to the platform's local placeholder.
|
|
156
|
+
return sessionGated
|
|
157
|
+
? createStarterSessionGatedGameServices({
|
|
158
|
+
...servicesInput,
|
|
159
|
+
signIn: guestSignIn,
|
|
160
|
+
// The startup recovery the boot ran without a player, with the action coordinator's.
|
|
161
|
+
onActivated: () => {
|
|
162
|
+
void gameRuntime?.reconcile().catch((error: unknown) => {
|
|
163
|
+
console.error('[game-services] recovery after sign-in failed', error);
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
: createStarterGameServices({
|
|
168
|
+
...servicesInput,
|
|
169
|
+
playerId: identitySession.playerId ?? player.playerId,
|
|
170
|
+
});
|
|
171
|
+
},
|
|
112
172
|
});
|
|
113
173
|
gameRuntime = ownedRuntime;
|
|
114
174
|
const gameServices = ownedRuntime.services;
|
|
@@ -149,6 +209,7 @@ async function bootstrap(options: { readonly monetization?: StarterMonetizationP
|
|
|
149
209
|
});
|
|
150
210
|
} catch (error) {
|
|
151
211
|
gameRuntime?.dispose();
|
|
212
|
+
disposeGuestSignIn?.();
|
|
152
213
|
disposeMicrosoftStorePwa?.();
|
|
153
214
|
renderBootstrapError(error, locale);
|
|
154
215
|
console.error('[bootstrap]', error);
|
|
@@ -8,23 +8,44 @@ import type { StorePurchaseAccountBinding } from '@mpgd/platform/commerce';
|
|
|
8
8
|
* (`resolveObfuscatedAccountId` of the Play verifier,
|
|
9
9
|
* `resolveAppAccountToken` of the App Store verifier), which is how the
|
|
10
10
|
* backend ties a store purchase to the player before it grants. This module
|
|
11
|
-
* derives both from a SHA-256 digest of the
|
|
12
|
-
*
|
|
11
|
+
* derives both from a SHA-256 digest of the server player id (the reference
|
|
12
|
+
* identity Worker, apps/game-identity-worker, derives the same); a game with
|
|
13
|
+
* its own account scheme replaces these two functions and the backend
|
|
13
14
|
* resolvers together. Nothing here grants anything.
|
|
14
15
|
*/
|
|
15
16
|
export const starterAccountBindingDigestPrefix = 'mpgd-commerce-account:';
|
|
16
17
|
|
|
18
|
+
/** The game's server player source; see `setStarterCommerceServerPlayer`. */
|
|
19
|
+
let serverPlayer: (() => string | undefined) | undefined;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Binds store purchases to the player of the game's server session (the
|
|
23
|
+
* identity service's player, a server-issued guest included) instead of the
|
|
24
|
+
* platform session. A generated game's `guestIdentity.ts` sets it when
|
|
25
|
+
* `VITE_MPGD_IDENTITY_URL` is configured; `undefined` restores the platform
|
|
26
|
+
* session. While it is set, no server player (not signed in yet, or the
|
|
27
|
+
* sign-in failed) binds nothing: the platform's local player never stands in.
|
|
28
|
+
*/
|
|
29
|
+
export function setStarterCommerceServerPlayer(source: (() => string | undefined) | undefined): void {
|
|
30
|
+
serverPlayer = source;
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
/**
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
* platform
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
34
|
+
* The player id a purchase is bound to: the server session's player when
|
|
35
|
+
* the game set one (`setStarterCommerceServerPlayer`), else an
|
|
36
|
+
* `authenticated` platform session whose trust is not `local`. A
|
|
37
|
+
* platform-anonymous session (the native shell's `android-local-player` /
|
|
38
|
+
* `ios-local-player`) or a locally asserted id would give every install the
|
|
39
|
+
* same binding, so it binds nothing; the backend binds the purchase to the
|
|
40
|
+
* player its ingress authentication resolved anyway.
|
|
24
41
|
*/
|
|
25
42
|
export async function resolveStarterCommercePlayerId(
|
|
26
43
|
gateway: Pick<PlatformGateway, 'identity'>,
|
|
27
44
|
): Promise<string | undefined> {
|
|
45
|
+
if (serverPlayer !== undefined) {
|
|
46
|
+
const playerId = serverPlayer()?.trim();
|
|
47
|
+
return playerId === undefined || playerId.length === 0 ? undefined : playerId;
|
|
48
|
+
}
|
|
28
49
|
let session: IdentitySession | undefined;
|
|
29
50
|
try {
|
|
30
51
|
session = await gateway.identity.getSession?.();
|
|
@@ -38,7 +59,11 @@ export async function resolveStarterCommercePlayerId(
|
|
|
38
59
|
return playerId === undefined || playerId.length === 0 ? undefined : playerId;
|
|
39
60
|
}
|
|
40
61
|
|
|
41
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Google Play `obfuscatedAccountId`: 64 hex characters, within Play's
|
|
64
|
+
* 64-character limit. `undefined` without a player, which the Play
|
|
65
|
+
* provider refuses with `ACCOUNT_BINDING_REQUIRED`.
|
|
66
|
+
*/
|
|
42
67
|
export async function resolvePlayBillingAccountBinding(
|
|
43
68
|
gateway: Pick<PlatformGateway, 'identity'>,
|
|
44
69
|
): Promise<StorePurchaseAccountBinding | undefined> {
|
|
@@ -55,7 +80,11 @@ export async function resolveStoreKitAppAccountToken(
|
|
|
55
80
|
): Promise<string> {
|
|
56
81
|
const playerId = await resolveStarterCommercePlayerId(gateway);
|
|
57
82
|
if (playerId === undefined) {
|
|
58
|
-
throw new Error(
|
|
83
|
+
throw new Error(
|
|
84
|
+
'A StoreKit purchase is bound to the player of a server session, and there is none: '
|
|
85
|
+
+ 'sign in to an identity service first (a generated game: VITE_MPGD_IDENTITY_URL and '
|
|
86
|
+
+ "src/platform/guestIdentity.ts). The native shell's local player cannot bind a purchase.",
|
|
87
|
+
);
|
|
59
88
|
}
|
|
60
89
|
return toUuid(await digestPlayerId(playerId));
|
|
61
90
|
}
|