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