@looplay/sdk 0.7.0 → 0.8.2

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.
@@ -334,10 +334,64 @@ var ServiceClient = class {
334
334
  headers: { "x-game-key": gameKey }
335
335
  });
336
336
  }
337
- // No purchase route is reachable from client/browser code — spending coin
338
- // balance always requires the creator+game secret pair
339
- // (`x-creator-key`/`x-game-secret`), called from the game's own backend.
340
- // This SDK intentionally never wires that tier up.
337
+ // ─────────────────────────────────────────────────────────────────────────────
338
+ // Self-serve purchase (public tier) same trust level as tracking: public
339
+ // `x-game-key` + the purchasing player's own bearer token, no backend or
340
+ // secret pair required. Safe to call directly from client/browser code —
341
+ // `checkoutToken` (short-lived, server-signed) plays the same role as
342
+ // `attemptToken` on the tracking routes. `ApiClient.purchaseStoreOffer`
343
+ // wraps this pair for you.
344
+ // ─────────────────────────────────────────────────────────────────────────────
345
+ /** Signs a `checkoutToken` for one purchase — required by `purchaseMyStoreOffer`. */
346
+ async createMyStoreCheckoutIntent(gameKey, bearerToken, body, trackingSessionId) {
347
+ return this.http.request("POST", "/sdk/games/store/checkout-intent/me", {
348
+ bearerToken,
349
+ headers: { "x-game-key": gameKey, "x-game-tracking-session-id": trackingSessionId },
350
+ body
351
+ });
352
+ }
353
+ async purchaseMyStoreOffer(gameKey, bearerToken, body, trackingSessionId) {
354
+ return this.http.request("POST", "/sdk/games/store/purchase/me", {
355
+ bearerToken,
356
+ headers: { "x-game-key": gameKey, "x-game-tracking-session-id": trackingSessionId },
357
+ body
358
+ });
359
+ }
360
+ // ─────────────────────────────────────────────────────────────────────────────
361
+ // Server-to-server (secret pair) — moves real coin balance. Requires the
362
+ // creator+game *secret* pair (`x-creator-key`/`x-game-secret`), distinct
363
+ // from the public `x-game-key` used everywhere above. Call these ONLY
364
+ // from your own backend (e.g. a Node service holding the secret pair) —
365
+ // never from client/browser code, or the secret is exposed to every
366
+ // player. `ApiClient` deliberately does not expose these — it auto-injects
367
+ // a *player's* access token for browser-facing calls and has no concept
368
+ // of your creator secret. Prefer the self-serve tier above unless you
369
+ // specifically need to initiate a purchase from your backend without the
370
+ // player present.
371
+ // ─────────────────────────────────────────────────────────────────────────────
372
+ /** Signs a `checkoutToken` for one purchase — required by `purchaseGameStoreOffer`. */
373
+ async createGameStoreCheckoutIntent(auth, body) {
374
+ return this.http.request("POST", "/sdk/games/store/checkout-intent", {
375
+ bearerToken: auth.bearerToken,
376
+ headers: {
377
+ "x-creator-key": auth.creatorKey,
378
+ "x-game-secret": auth.gameSecret,
379
+ "x-game-tracking-session-id": auth.trackingSessionId
380
+ },
381
+ body
382
+ });
383
+ }
384
+ async purchaseGameStoreOffer(auth, body) {
385
+ return this.http.request("POST", "/sdk/games/store/purchase", {
386
+ bearerToken: auth.bearerToken,
387
+ headers: {
388
+ "x-creator-key": auth.creatorKey,
389
+ "x-game-secret": auth.gameSecret,
390
+ "x-game-tracking-session-id": auth.trackingSessionId
391
+ },
392
+ body
393
+ });
394
+ }
341
395
  // ─────────────────────────────────────────────────────────────────────────────
342
396
  // Balance
343
397
  // ─────────────────────────────────────────────────────────────────────────────
@@ -416,6 +470,11 @@ var ApiClient = class {
416
470
  unsafeRaw() {
417
471
  return this.raw;
418
472
  }
473
+ // Real-time (balance_change / store_purchase_completed WebSocket events)
474
+ // deliberately lives outside this class — import `WsClient` from
475
+ // '@looplay/sdk/realtime' and construct it with the same `getAccessToken`
476
+ // you passed here. Keeps the socket.io-client dependency (and its bundle
477
+ // weight) out of every consumer that doesn't need real-time updates.
419
478
  async requireToken() {
420
479
  const token = await this.getAccessToken?.();
421
480
  if (!token) throw new NotAuthenticatedError();
@@ -554,10 +613,27 @@ var ApiClient = class {
554
613
  const token = await this.requireToken();
555
614
  return this.raw.listMyStorePurchases(token, gameKey);
556
615
  }
557
- // There is no purchaseStoreOffer() here by design — spending coin balance
558
- // requires the creator+game secret pair, which must be called from the
559
- // game's own backend, never from this client-side SDK. Implement a
560
- // checkout endpoint on your backend that calls gbs-service directly.
616
+ /**
617
+ * Spends coin balance to purchase a store offer signs a checkout intent
618
+ * first, then redeems it, mirroring the gameplay attempt flow above.
619
+ * Self-serve: only the player's own access token is used, no secret pair
620
+ * required, safe to call directly from the game client. Also see
621
+ * `onStorePurchase` to react to the result in real time from any tab.
622
+ */
623
+ async purchaseStoreOffer(gameKey, offerCode, opts) {
624
+ const token = await this.requireToken();
625
+ const intent = await this.raw.createMyStoreCheckoutIntent(gameKey, token, {
626
+ offerCode,
627
+ requestId: opts?.requestId,
628
+ quantity: opts?.quantity
629
+ });
630
+ return this.raw.purchaseMyStoreOffer(gameKey, token, {
631
+ offerCode: intent.offerCode,
632
+ requestId: intent.requestId,
633
+ quantity: intent.quantity,
634
+ checkoutToken: intent.checkoutToken
635
+ });
636
+ }
561
637
  async getMyProfile() {
562
638
  const token = await this.requireToken();
563
639
  return this.raw.getMyProfile(token);