@mpgd/game-services 0.8.2 → 0.10.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/client.js CHANGED
@@ -29,7 +29,7 @@ export function createGameServicesClient(input) {
29
29
  return {
30
30
  async purchase(purchaseInput) {
31
31
  const target = input.target;
32
- if (!isGameServicesStoreTarget(target)) {
32
+ if (!isGameServicesCommerceTarget(target)) {
33
33
  const purchase = {
34
34
  status: 'failed',
35
35
  entitlementIds: [],
@@ -48,6 +48,25 @@ export function createGameServicesClient(input) {
48
48
  };
49
49
  }
50
50
  const purchase = await input.gateway.commerce.purchase(purchaseInput);
51
+ if (target === 'verse8') {
52
+ const status = purchase.status === 'completed' ? 'rejected' : purchase.status;
53
+ await analytics.track({
54
+ name: 'purchase_rejected',
55
+ properties: {
56
+ productId: purchaseInput.productId,
57
+ status,
58
+ reason: purchase.status === 'completed'
59
+ ? 'verse8_grants_require_agent8_purchase_event'
60
+ : purchase.status === 'pending'
61
+ ? 'agent8_purchase_event_pending'
62
+ : undefined,
63
+ },
64
+ });
65
+ return {
66
+ status,
67
+ purchase,
68
+ };
69
+ }
51
70
  if (purchase.status !== 'completed' || purchase.transactionId === undefined) {
52
71
  await analytics.track({
53
72
  name: 'purchase_rejected',
@@ -93,7 +112,7 @@ export function createGameServicesClient(input) {
93
112
  },
94
113
  async claimRewardedAd(rewardInput) {
95
114
  const target = input.target;
96
- if (!isGameServicesStoreTarget(target)) {
115
+ if (!isGameServicesAdRewardTarget(target)) {
97
116
  const reward = {
98
117
  status: 'unavailable',
99
118
  rewardGranted: false,
@@ -157,6 +176,23 @@ export function createGameServicesClient(input) {
157
176
  return result;
158
177
  },
159
178
  async submitLeaderboardScore(scoreInput) {
179
+ const target = input.target;
180
+ if (!isGameServicesLeaderboardTarget(target)) {
181
+ await analytics.track({
182
+ name: 'leaderboard_submitted',
183
+ properties: {
184
+ leaderboardId: scoreInput.leaderboardId,
185
+ score: scoreInput.score,
186
+ submitted: false,
187
+ reason: 'unsupported_target',
188
+ },
189
+ });
190
+ return {
191
+ submitted: false,
192
+ platformSubmitted: false,
193
+ alreadyProcessed: false,
194
+ };
195
+ }
160
196
  const platformResult = await input.gateway.leaderboard.submitScore(scoreInput);
161
197
  if (!platformResult.submitted) {
162
198
  await analytics.track({
@@ -174,7 +210,7 @@ export function createGameServicesClient(input) {
174
210
  };
175
211
  }
176
212
  const record = await input.backend.leaderboard.recordScore({
177
- target: input.target,
213
+ target,
178
214
  playerId: input.playerId,
179
215
  ...scoreInput,
180
216
  });
@@ -281,8 +317,18 @@ export function createGameServicesIdempotencyKey(input) {
281
317
  function normalizeSegment(value) {
282
318
  return value.replaceAll(/[^a-zA-Z0-9_-]+/g, '-').replaceAll(/^-|-$/g, '').slice(0, 64);
283
319
  }
284
- function isGameServicesStoreTarget(target) {
285
- return target === 'android' || target === 'ios' || target === 'ait';
320
+ function isGameServicesCommerceTarget(target) {
321
+ return target === 'android' || target === 'ios' || target === 'ait' || target === 'verse8';
322
+ }
323
+ function isGameServicesAdRewardTarget(target) {
324
+ return target === 'android' || target === 'ios' || target === 'ait' || target === 'verse8';
325
+ }
326
+ function isGameServicesLeaderboardTarget(target) {
327
+ return target === 'browser'
328
+ || target === 'android'
329
+ || target === 'ios'
330
+ || target === 'ait'
331
+ || target === 'reddit';
286
332
  }
287
333
  async function sendGameServicesBackendRequest(transport, endpoint, body) {
288
334
  const response = await transport.send({
@@ -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;