@mpgd/game-services 0.9.0 → 0.11.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/admob-ssv-conformance.d.ts +19 -0
- package/dist/admob-ssv-conformance.js +258 -0
- package/dist/admob-ssv.d.ts +54 -0
- package/dist/admob-ssv.js +566 -0
- package/dist/app-store-verifier-conformance.d.ts +25 -0
- package/dist/app-store-verifier-conformance.js +28 -0
- package/dist/app-store-verifier.d.ts +87 -0
- package/dist/app-store-verifier.js +439 -0
- package/dist/apps-in-toss-evidence-verification-conformance.d.ts +26 -0
- package/dist/apps-in-toss-evidence-verification-conformance.js +613 -0
- package/dist/apps-in-toss-evidence-verification.d.ts +162 -0
- package/dist/apps-in-toss-evidence-verification.js +458 -0
- package/dist/apps-in-toss-partner-api.d.ts +48 -0
- package/dist/apps-in-toss-partner-api.js +282 -0
- package/dist/evidence-verification.d.ts +21 -1
- package/dist/google-play-purchase-conformance.d.ts +26 -0
- package/dist/google-play-purchase-conformance.js +39 -0
- package/dist/google-play-purchase.d.ts +40 -0
- package/dist/google-play-purchase.js +559 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/server.d.ts +3 -1
- package/dist/server.js +199 -14
- package/dist/types.d.ts +10 -0
- package/dist/types.js +32 -0
- package/package.json +42 -5
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
export const appsInTossPartnerApiBaseUrl = 'https://apps-in-toss-api.toss.im';
|
|
2
|
+
export const defaultAppsInTossPartnerApiTimeoutMs = 10000;
|
|
3
|
+
const verifyAnonymousKeyPath = 'api-partner/v1/apps-in-toss/users/anon-key/verify';
|
|
4
|
+
const sendFunctionalMessagePath = 'api-partner/v1/apps-in-toss/messenger/send-message';
|
|
5
|
+
const maximumResponseBodyBytes = 256 * 1024;
|
|
6
|
+
export class AppsInTossPartnerApiError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
code;
|
|
9
|
+
name = 'AppsInTossPartnerApiError';
|
|
10
|
+
constructor(message, status, code, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function createAppsInTossPartnerApiClient(input) {
|
|
17
|
+
const baseUrl = normalizeBaseUrl(input.baseUrl ?? appsInTossPartnerApiBaseUrl);
|
|
18
|
+
const timeoutMs = normalizeTimeout(input.timeoutMs);
|
|
19
|
+
return {
|
|
20
|
+
async verifyAnonymousKey(request) {
|
|
21
|
+
const anonymousKey = normalizeIdentifier(request.anonymousKey, 'anonymousKey');
|
|
22
|
+
const response = await postJson({
|
|
23
|
+
mtls: input.mtls,
|
|
24
|
+
url: new URL(verifyAnonymousKeyPath, baseUrl).href,
|
|
25
|
+
headers: { 'x-anon-key': anonymousKey },
|
|
26
|
+
body: undefined,
|
|
27
|
+
timeoutMs,
|
|
28
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
29
|
+
});
|
|
30
|
+
if (response.status === 401) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const envelope = requireSuccessEnvelope(response);
|
|
34
|
+
if (envelope.success !== 'true') {
|
|
35
|
+
throw new AppsInTossPartnerApiError('Apps in Toss returned an invalid anonymous-key verification response.', response.status);
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
async sendFunctionalMessage(request) {
|
|
40
|
+
const recipient = normalizeRecipient(request.recipient);
|
|
41
|
+
const templateSetCode = normalizeIdentifier(request.templateSetCode, 'templateSetCode');
|
|
42
|
+
const response = await postJson({
|
|
43
|
+
mtls: input.mtls,
|
|
44
|
+
url: new URL(sendFunctionalMessagePath, baseUrl).href,
|
|
45
|
+
headers: recipient.type === 'anonymous'
|
|
46
|
+
? { 'x-anon-key': recipient.key }
|
|
47
|
+
: { 'x-toss-user-key': recipient.key },
|
|
48
|
+
body: {
|
|
49
|
+
templateSetCode,
|
|
50
|
+
context: normalizeContext(request.context),
|
|
51
|
+
},
|
|
52
|
+
timeoutMs,
|
|
53
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
54
|
+
});
|
|
55
|
+
const envelope = requireSuccessEnvelope(response);
|
|
56
|
+
return parseFunctionalMessageResult(envelope.success, response.status);
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function postJson(input) {
|
|
61
|
+
const timeout = createTimeoutSignal(input.signal, input.timeoutMs);
|
|
62
|
+
try {
|
|
63
|
+
// The documented anonymous-key verification request uses an empty body
|
|
64
|
+
// while still declaring application/json.
|
|
65
|
+
const requestBody = input.body === undefined ? '' : JSON.stringify(input.body);
|
|
66
|
+
const headers = new Headers({
|
|
67
|
+
accept: 'application/json',
|
|
68
|
+
'content-type': 'application/json',
|
|
69
|
+
...input.headers,
|
|
70
|
+
});
|
|
71
|
+
let response;
|
|
72
|
+
let text;
|
|
73
|
+
try {
|
|
74
|
+
response = await input.mtls.fetch(input.url, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers,
|
|
77
|
+
body: requestBody,
|
|
78
|
+
signal: timeout.signal,
|
|
79
|
+
});
|
|
80
|
+
text = await readBoundedResponseText(response);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw normalizeTransportError(error, input.signal, timeout.signal);
|
|
84
|
+
}
|
|
85
|
+
let body;
|
|
86
|
+
try {
|
|
87
|
+
body = text.length === 0 ? undefined : JSON.parse(text);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new AppsInTossPartnerApiError('Apps in Toss returned a non-JSON response.', response.status);
|
|
91
|
+
}
|
|
92
|
+
return { status: response.status, ok: response.ok, body };
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
timeout.cleanup();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function normalizeTransportError(error, upstream, effective) {
|
|
99
|
+
if (error instanceof AppsInTossPartnerApiError) {
|
|
100
|
+
return error;
|
|
101
|
+
}
|
|
102
|
+
if (effective.aborted && upstream?.aborted !== true) {
|
|
103
|
+
return new AppsInTossPartnerApiError('Apps in Toss partner API request timed out.', 0, 'TIMEOUT', { cause: error });
|
|
104
|
+
}
|
|
105
|
+
if (upstream?.aborted === true) {
|
|
106
|
+
return new AppsInTossPartnerApiError('Apps in Toss partner API request was aborted.', 0, 'ABORTED', { cause: error });
|
|
107
|
+
}
|
|
108
|
+
return new AppsInTossPartnerApiError('Apps in Toss partner API transport failed.', 0, 'TRANSPORT_ERROR', { cause: error });
|
|
109
|
+
}
|
|
110
|
+
async function readBoundedResponseText(response) {
|
|
111
|
+
const declaredLength = response.headers.get('content-length');
|
|
112
|
+
if (declaredLength !== null
|
|
113
|
+
&& /^\d+$/u.test(declaredLength)
|
|
114
|
+
&& Number(declaredLength) > maximumResponseBodyBytes) {
|
|
115
|
+
throw responseTooLarge(response.status);
|
|
116
|
+
}
|
|
117
|
+
if (response.body === null) {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
const reader = response.body.getReader();
|
|
121
|
+
const chunks = [];
|
|
122
|
+
let byteLength = 0;
|
|
123
|
+
try {
|
|
124
|
+
while (true) {
|
|
125
|
+
const chunk = await reader.read();
|
|
126
|
+
if (chunk.done) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
byteLength += chunk.value.byteLength;
|
|
130
|
+
if (byteLength > maximumResponseBodyBytes) {
|
|
131
|
+
await reader.cancel().catch(() => { });
|
|
132
|
+
throw responseTooLarge(response.status);
|
|
133
|
+
}
|
|
134
|
+
chunks.push(chunk.value);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
reader.releaseLock();
|
|
139
|
+
}
|
|
140
|
+
const bytes = new Uint8Array(byteLength);
|
|
141
|
+
let offset = 0;
|
|
142
|
+
for (const chunk of chunks) {
|
|
143
|
+
bytes.set(chunk, offset);
|
|
144
|
+
offset += chunk.byteLength;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new AppsInTossPartnerApiError('Apps in Toss returned a non-UTF-8 response.', response.status);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function responseTooLarge(status) {
|
|
154
|
+
return new AppsInTossPartnerApiError('Apps in Toss response exceeded the maximum accepted size.', status);
|
|
155
|
+
}
|
|
156
|
+
function requireSuccessEnvelope(response) {
|
|
157
|
+
if (!isRecord(response.body)) {
|
|
158
|
+
throw new AppsInTossPartnerApiError('Apps in Toss returned an invalid response envelope.', response.status);
|
|
159
|
+
}
|
|
160
|
+
if (response.ok && response.body.resultType === 'SUCCESS') {
|
|
161
|
+
return response.body;
|
|
162
|
+
}
|
|
163
|
+
const error = isRecord(response.body.error) ? response.body.error : undefined;
|
|
164
|
+
const code = typeof error?.errorCode === 'string' ? error.errorCode : undefined;
|
|
165
|
+
const reason = typeof error?.reason === 'string'
|
|
166
|
+
? error.reason.slice(0, 1024)
|
|
167
|
+
: 'Apps in Toss partner API request failed.';
|
|
168
|
+
throw new AppsInTossPartnerApiError(reason, response.status, code);
|
|
169
|
+
}
|
|
170
|
+
function parseFunctionalMessageResult(input, status) {
|
|
171
|
+
if (!isRecord(input)) {
|
|
172
|
+
throw new AppsInTossPartnerApiError('Apps in Toss returned an invalid message result.', status);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
messageCount: requireCount(input.msgCount, 'msgCount', status),
|
|
176
|
+
sentPushCount: requireCount(input.sentPushCount, 'sentPushCount', status),
|
|
177
|
+
sentInboxCount: requireCount(input.sentInboxCount, 'sentInboxCount', status),
|
|
178
|
+
sentSmsCount: requireCount(input.sentSmsCount, 'sentSmsCount', status),
|
|
179
|
+
sentAlimtalkCount: requireCount(input.sentAlimtalkCount, 'sentAlimtalkCount', status),
|
|
180
|
+
sentFriendtalkCount: requireCount(input.sentFriendtalkCount, 'sentFriendtalkCount', status),
|
|
181
|
+
contentIds: readContentIds(input.detail),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function readContentIds(detail) {
|
|
185
|
+
if (!isRecord(detail)) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
const ids = new Set();
|
|
189
|
+
for (const channel of ['sentPush', 'sentInbox', 'sentSms', 'sentAlimtalk', 'sentFriendtalk']) {
|
|
190
|
+
const entries = detail[channel];
|
|
191
|
+
if (!Array.isArray(entries)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
if (isRecord(entry) && typeof entry.contentId === 'string' && entry.contentId.length > 0) {
|
|
196
|
+
ids.add(entry.contentId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return [...ids];
|
|
201
|
+
}
|
|
202
|
+
function requireCount(value, field, status) {
|
|
203
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
204
|
+
throw new AppsInTossPartnerApiError(`Apps in Toss returned an invalid ${field}.`, status);
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
function normalizeRecipient(input) {
|
|
209
|
+
if (input.type !== 'anonymous' && input.type !== 'toss-user') {
|
|
210
|
+
throw new TypeError('AIT message recipient type is invalid.');
|
|
211
|
+
}
|
|
212
|
+
return { type: input.type, key: normalizeIdentifier(input.key, 'recipient.key') };
|
|
213
|
+
}
|
|
214
|
+
function normalizeContext(input) {
|
|
215
|
+
const entries = Object.entries(input);
|
|
216
|
+
if (entries.length > 128) {
|
|
217
|
+
throw new TypeError('AIT message context cannot contain more than 128 values.');
|
|
218
|
+
}
|
|
219
|
+
const output = Object.create(null);
|
|
220
|
+
for (const [key, value] of entries) {
|
|
221
|
+
const normalizedKey = normalizeIdentifier(key, 'context key');
|
|
222
|
+
if (typeof value !== 'string'
|
|
223
|
+
&& typeof value !== 'number'
|
|
224
|
+
&& typeof value !== 'boolean') {
|
|
225
|
+
throw new TypeError(`AIT message context value is invalid: ${normalizedKey}`);
|
|
226
|
+
}
|
|
227
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
228
|
+
throw new TypeError(`AIT message context number is invalid: ${normalizedKey}`);
|
|
229
|
+
}
|
|
230
|
+
output[normalizedKey] = value;
|
|
231
|
+
}
|
|
232
|
+
return output;
|
|
233
|
+
}
|
|
234
|
+
function normalizeIdentifier(value, field) {
|
|
235
|
+
const normalized = value.trim();
|
|
236
|
+
if (normalized.length === 0
|
|
237
|
+
|| normalized.length > 2048
|
|
238
|
+
|| /[\p{Cc}\p{Cf}]/u.test(normalized)) {
|
|
239
|
+
throw new TypeError(`${field} must contain 1 to 2048 characters without control or format characters.`);
|
|
240
|
+
}
|
|
241
|
+
return normalized;
|
|
242
|
+
}
|
|
243
|
+
function normalizeBaseUrl(input) {
|
|
244
|
+
const url = new URL(input);
|
|
245
|
+
if (url.protocol !== 'https:'
|
|
246
|
+
|| url.username.length > 0
|
|
247
|
+
|| url.password.length > 0
|
|
248
|
+
|| url.search.length > 0
|
|
249
|
+
|| url.hash.length > 0) {
|
|
250
|
+
throw new TypeError('AIT partner API baseUrl must be a credential-free HTTPS URL.');
|
|
251
|
+
}
|
|
252
|
+
url.pathname = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`;
|
|
253
|
+
return url;
|
|
254
|
+
}
|
|
255
|
+
function normalizeTimeout(value) {
|
|
256
|
+
const timeoutMs = value ?? defaultAppsInTossPartnerApiTimeoutMs;
|
|
257
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 60000) {
|
|
258
|
+
throw new TypeError('AIT partner API timeoutMs must be between 1 and 60000.');
|
|
259
|
+
}
|
|
260
|
+
return timeoutMs;
|
|
261
|
+
}
|
|
262
|
+
function createTimeoutSignal(upstream, timeoutMs) {
|
|
263
|
+
const controller = new AbortController();
|
|
264
|
+
const abortFromUpstream = () => controller.abort(upstream?.reason);
|
|
265
|
+
if (upstream?.aborted === true) {
|
|
266
|
+
abortFromUpstream();
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
upstream?.addEventListener('abort', abortFromUpstream, { once: true });
|
|
270
|
+
}
|
|
271
|
+
const timer = globalThis.setTimeout(() => controller.abort(new Error('Apps in Toss partner API request timed out.')), timeoutMs);
|
|
272
|
+
return {
|
|
273
|
+
signal: controller.signal,
|
|
274
|
+
cleanup() {
|
|
275
|
+
globalThis.clearTimeout(timer);
|
|
276
|
+
upstream?.removeEventListener('abort', abortFromUpstream);
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function isRecord(value) {
|
|
281
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
282
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { AdPlacements, ProductCatalog } from '@mpgd/catalog';
|
|
2
|
-
import type { ClaimAdRewardRequest, EntitlementLedgerPayload, VerifyPurchaseRequest } from './types';
|
|
2
|
+
import type { ClaimAdRewardRequest, EntitlementLedgerPayload, PurchaseGrantFinalization, VerifyPurchaseRequest } from './types';
|
|
3
3
|
export type EvidenceVerificationDecision = {
|
|
4
4
|
readonly status: 'verified';
|
|
5
5
|
readonly verificationId: string;
|
|
6
6
|
readonly verifiedAt: string;
|
|
7
7
|
readonly payload?: EntitlementLedgerPayload;
|
|
8
|
+
/**
|
|
9
|
+
* An authoritative provider identity for cross-idempotency replay checks.
|
|
10
|
+
* `null` explicitly suppresses the client-reported platform identity.
|
|
11
|
+
*/
|
|
12
|
+
readonly platformEvidenceId?: string | null;
|
|
8
13
|
} | {
|
|
9
14
|
readonly status: 'pending';
|
|
10
15
|
readonly reason?: string;
|
|
@@ -26,6 +31,21 @@ export interface VerifyAdRewardEvidenceInput {
|
|
|
26
31
|
readonly signal: AbortSignal;
|
|
27
32
|
readonly timeoutMs: number;
|
|
28
33
|
}
|
|
34
|
+
export interface FinalizePurchaseGrantInput {
|
|
35
|
+
readonly request: VerifyPurchaseRequest;
|
|
36
|
+
readonly product: ProductCatalog['products'][number];
|
|
37
|
+
readonly platformProductId: string;
|
|
38
|
+
readonly evidenceVerificationId: string;
|
|
39
|
+
readonly evidencePayload?: EntitlementLedgerPayload;
|
|
40
|
+
readonly ledgerEntryId: string;
|
|
41
|
+
readonly alreadyProcessed: boolean;
|
|
42
|
+
readonly signal: AbortSignal;
|
|
43
|
+
readonly timeoutMs: number;
|
|
44
|
+
}
|
|
45
|
+
export interface GameServicesPurchaseGrantFinalizer {
|
|
46
|
+
supportsPurchaseGrant?(input: Omit<FinalizePurchaseGrantInput, 'signal'>): boolean;
|
|
47
|
+
finalizePurchaseGrant(input: FinalizePurchaseGrantInput): Promise<PurchaseGrantFinalization>;
|
|
48
|
+
}
|
|
29
49
|
export interface GameServicesEvidenceVerifier {
|
|
30
50
|
verifyPurchase(input: VerifyPurchaseEvidenceInput): Promise<EvidenceVerificationDecision>;
|
|
31
51
|
verifyAdReward(input: VerifyAdRewardEvidenceInput): Promise<EvidenceVerificationDecision>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type GooglePlayAcknowledgementState, type GooglePlayConsumptionState, type GooglePlayPurchaseState } from './google-play-purchase';
|
|
2
|
+
export declare const googlePlayProductPurchaseConformanceEvidence: Readonly<{
|
|
3
|
+
schema: "google-play.product-purchase.v2";
|
|
4
|
+
payload: Readonly<{
|
|
5
|
+
purchaseToken: "google-play-conformance-token";
|
|
6
|
+
}>;
|
|
7
|
+
}>;
|
|
8
|
+
export interface CreateGooglePlayProductPurchaseConformanceFixtureInput {
|
|
9
|
+
readonly productId?: string;
|
|
10
|
+
readonly orderId?: string;
|
|
11
|
+
readonly purchaseState?: Exclude<GooglePlayPurchaseState, 'PURCHASE_STATE_UNSPECIFIED'>;
|
|
12
|
+
readonly purchaseCompletionTime?: string;
|
|
13
|
+
readonly consumptionState?: Exclude<GooglePlayConsumptionState, 'CONSUMPTION_STATE_UNSPECIFIED'>;
|
|
14
|
+
readonly acknowledgementState?: Exclude<GooglePlayAcknowledgementState, 'ACKNOWLEDGEMENT_STATE_UNSPECIFIED'>;
|
|
15
|
+
readonly quantity?: number;
|
|
16
|
+
readonly refundableQuantity?: number;
|
|
17
|
+
readonly rentOffer?: boolean;
|
|
18
|
+
readonly obfuscatedExternalAccountId?: string;
|
|
19
|
+
readonly obfuscatedExternalProfileId?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Builds a deterministic `purchases.productsv2` response for boundary and
|
|
23
|
+
* provider-client conformance tests. It intentionally contains no credentials,
|
|
24
|
+
* live purchase tokens, or environment-specific endpoints.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createGooglePlayProductPurchaseConformanceFixture(input?: CreateGooglePlayProductPurchaseConformanceFixtureInput): Readonly<Record<string, unknown>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { googlePlayProductPurchaseEvidenceSchema, } from './google-play-purchase';
|
|
2
|
+
export const googlePlayProductPurchaseConformanceEvidence = Object.freeze({
|
|
3
|
+
schema: googlePlayProductPurchaseEvidenceSchema,
|
|
4
|
+
payload: Object.freeze({
|
|
5
|
+
purchaseToken: 'google-play-conformance-token',
|
|
6
|
+
}),
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Builds a deterministic `purchases.productsv2` response for boundary and
|
|
10
|
+
* provider-client conformance tests. It intentionally contains no credentials,
|
|
11
|
+
* live purchase tokens, or environment-specific endpoints.
|
|
12
|
+
*/
|
|
13
|
+
export function createGooglePlayProductPurchaseConformanceFixture(input = {}) {
|
|
14
|
+
return {
|
|
15
|
+
productLineItem: [
|
|
16
|
+
{
|
|
17
|
+
productId: input.productId ?? 'coins_100_android',
|
|
18
|
+
productOfferDetails: {
|
|
19
|
+
quantity: input.quantity ?? 1,
|
|
20
|
+
refundableQuantity: input.refundableQuantity ?? 1,
|
|
21
|
+
consumptionState: input.consumptionState ?? 'CONSUMPTION_STATE_YET_TO_BE_CONSUMED',
|
|
22
|
+
...(input.rentOffer === true ? { rentOfferDetails: {} } : {}),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
purchaseStateContext: {
|
|
27
|
+
purchaseState: input.purchaseState ?? 'PURCHASED',
|
|
28
|
+
},
|
|
29
|
+
orderId: input.orderId ?? 'GPA.conformance-1',
|
|
30
|
+
purchaseCompletionTime: input.purchaseCompletionTime ?? '2030-01-02T03:04:05.000Z',
|
|
31
|
+
acknowledgementState: input.acknowledgementState ?? 'ACKNOWLEDGEMENT_STATE_PENDING',
|
|
32
|
+
...(input.obfuscatedExternalAccountId === undefined
|
|
33
|
+
? {}
|
|
34
|
+
: { obfuscatedExternalAccountId: input.obfuscatedExternalAccountId }),
|
|
35
|
+
...(input.obfuscatedExternalProfileId === undefined
|
|
36
|
+
? {}
|
|
37
|
+
: { obfuscatedExternalProfileId: input.obfuscatedExternalProfileId }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { GameServicesEvidenceVerifier, GameServicesPurchaseGrantFinalizer, VerifyPurchaseEvidenceInput } from './evidence-verification';
|
|
2
|
+
export declare const googlePlayProductPurchaseEvidenceSchema = "google-play.product-purchase.v2";
|
|
3
|
+
export type GooglePlayPurchaseState = 'PURCHASED' | 'CANCELLED' | 'PENDING' | 'PURCHASE_STATE_UNSPECIFIED';
|
|
4
|
+
export type GooglePlayConsumptionState = 'CONSUMPTION_STATE_YET_TO_BE_CONSUMED' | 'CONSUMPTION_STATE_CONSUMED' | 'CONSUMPTION_STATE_UNSPECIFIED';
|
|
5
|
+
export type GooglePlayAcknowledgementState = 'ACKNOWLEDGEMENT_STATE_PENDING' | 'ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED' | 'ACKNOWLEDGEMENT_STATE_UNSPECIFIED';
|
|
6
|
+
export interface GooglePlayProductPurchaseClient {
|
|
7
|
+
getProductPurchaseV2(input: {
|
|
8
|
+
readonly packageName: string;
|
|
9
|
+
readonly purchaseToken: string;
|
|
10
|
+
readonly signal: AbortSignal;
|
|
11
|
+
}): Promise<unknown>;
|
|
12
|
+
acknowledgeProductPurchase(input: {
|
|
13
|
+
readonly packageName: string;
|
|
14
|
+
readonly productId: string;
|
|
15
|
+
readonly purchaseToken: string;
|
|
16
|
+
readonly signal: AbortSignal;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
consumeProductPurchase(input: {
|
|
19
|
+
readonly packageName: string;
|
|
20
|
+
readonly productId: string;
|
|
21
|
+
readonly purchaseToken: string;
|
|
22
|
+
readonly signal: AbortSignal;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface CreateGooglePlayProductPurchaseBoundaryInput {
|
|
26
|
+
readonly client: GooglePlayProductPurchaseClient;
|
|
27
|
+
readonly packageName: string;
|
|
28
|
+
readonly now?: () => string;
|
|
29
|
+
/**
|
|
30
|
+
* Explicitly permits purchases without a Google account identifier. Use only
|
|
31
|
+
* when the caller binds `playerId` to an authenticated application account.
|
|
32
|
+
*/
|
|
33
|
+
readonly allowUnboundAuthenticatedPlayer?: boolean;
|
|
34
|
+
readonly resolveObfuscatedAccountId?: (playerId: string, signal: AbortSignal) => Promise<string | undefined> | string | undefined;
|
|
35
|
+
readonly resolveObfuscatedProfileId?: (playerId: string, signal: AbortSignal) => Promise<string | undefined> | string | undefined;
|
|
36
|
+
}
|
|
37
|
+
export interface GooglePlayProductPurchaseBoundary extends GameServicesPurchaseGrantFinalizer {
|
|
38
|
+
verifyPurchase(input: VerifyPurchaseEvidenceInput): ReturnType<GameServicesEvidenceVerifier['verifyPurchase']>;
|
|
39
|
+
}
|
|
40
|
+
export declare function createGooglePlayProductPurchaseBoundary(input: CreateGooglePlayProductPurchaseBoundaryInput): GooglePlayProductPurchaseBoundary;
|