@genex-ai/embed-sdk 0.14.0 → 0.15.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/{chunk-5SVRR26C.js → chunk-DWDBRKRS.js} +124 -5
- package/dist/index.d.ts +108 -1
- package/dist/index.js +9 -1
- package/dist/sentry.js +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,110 @@
|
|
|
1
|
+
// src/commerce.ts
|
|
2
|
+
var cfg = null;
|
|
3
|
+
function _initCommerce(c) {
|
|
4
|
+
cfg = c;
|
|
5
|
+
}
|
|
6
|
+
function must() {
|
|
7
|
+
if (!cfg) throw new Error("genex commerce: call initEmbed() first");
|
|
8
|
+
return cfg;
|
|
9
|
+
}
|
|
10
|
+
function authHeaders() {
|
|
11
|
+
const token = must().getToken();
|
|
12
|
+
if (!token) throw new Error("genex commerce: no player session");
|
|
13
|
+
return { Authorization: `Bearer ${token}` };
|
|
14
|
+
}
|
|
15
|
+
async function getShop() {
|
|
16
|
+
const c = must();
|
|
17
|
+
if (c.isLocalTest()) return [];
|
|
18
|
+
const res = await fetch(`${c.apiUrl}/api/coin/catalog`, { headers: authHeaders() });
|
|
19
|
+
if (!res.ok) throw new Error(`genex shop failed (${res.status})`);
|
|
20
|
+
return (await res.json()).items;
|
|
21
|
+
}
|
|
22
|
+
async function getEntitlements(opts) {
|
|
23
|
+
const c = must();
|
|
24
|
+
if (c.isLocalTest()) return [];
|
|
25
|
+
const qs = opts?.excludeConsumed ? "?excludeConsumed=true" : "";
|
|
26
|
+
const res = await fetch(`${c.apiUrl}/api/coin/entitlements${qs}`, { headers: authHeaders() });
|
|
27
|
+
if (!res.ok) throw new Error(`genex entitlements failed (${res.status})`);
|
|
28
|
+
return (await res.json()).items;
|
|
29
|
+
}
|
|
30
|
+
async function consumeEntitlement(entitlementId) {
|
|
31
|
+
const c = must();
|
|
32
|
+
if (c.isLocalTest()) return { consumed: true, alreadyConsumed: false };
|
|
33
|
+
const res = await fetch(
|
|
34
|
+
`${c.apiUrl}/api/coin/entitlements/${encodeURIComponent(entitlementId)}/consume`,
|
|
35
|
+
{ method: "POST", headers: authHeaders() }
|
|
36
|
+
);
|
|
37
|
+
if (!res.ok) throw new Error(`genex consume failed (${res.status})`);
|
|
38
|
+
return await res.json();
|
|
39
|
+
}
|
|
40
|
+
async function awaitOutcome(intentId, timeoutMs) {
|
|
41
|
+
const c = must();
|
|
42
|
+
const deadline = Date.now() + timeoutMs;
|
|
43
|
+
let delay = 400;
|
|
44
|
+
while (Date.now() < deadline) {
|
|
45
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
46
|
+
delay = Math.min(delay * 1.4, 2e3);
|
|
47
|
+
const res = await fetch(`${c.apiUrl}/api/coin/intents/${encodeURIComponent(intentId)}`, {
|
|
48
|
+
headers: authHeaders()
|
|
49
|
+
}).catch(() => null);
|
|
50
|
+
if (!res || !res.ok) continue;
|
|
51
|
+
const view = await res.json();
|
|
52
|
+
switch (view.status) {
|
|
53
|
+
case "succeeded":
|
|
54
|
+
case "consumed":
|
|
55
|
+
return { status: "succeeded" };
|
|
56
|
+
case "canceled":
|
|
57
|
+
return { status: "canceled" };
|
|
58
|
+
case "expired":
|
|
59
|
+
return { status: "expired" };
|
|
60
|
+
case "failed":
|
|
61
|
+
return { status: "failed", message: "the purchase could not be completed" };
|
|
62
|
+
default:
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { status: "expired" };
|
|
67
|
+
}
|
|
68
|
+
function openConfirmSurface(view) {
|
|
69
|
+
const c = must();
|
|
70
|
+
const w = typeof window === "undefined" ? null : window;
|
|
71
|
+
if (!w) return { ok: false, reason: "no_window" };
|
|
72
|
+
if (c.isNative()) return { ok: false, reason: "native_unsupported" };
|
|
73
|
+
if (c.isEmbedded() && w.parent) {
|
|
74
|
+
for (const origin of c.dashboardOrigins) {
|
|
75
|
+
try {
|
|
76
|
+
w.parent.postMessage({ type: "genex:commerce:confirm", v: 1, intentId: view.id }, origin);
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { ok: true };
|
|
81
|
+
}
|
|
82
|
+
const popup = w.open(view.confirmUrl, `genex_confirm_${view.id}`, "popup,width=420,height=640");
|
|
83
|
+
return popup === null ? { ok: false, reason: "popup_blocked" } : { ok: true };
|
|
84
|
+
}
|
|
85
|
+
async function buy(opts) {
|
|
86
|
+
const c = must();
|
|
87
|
+
if (c.isLocalTest()) {
|
|
88
|
+
return { status: "failed", message: "purchases are unavailable in local test mode" };
|
|
89
|
+
}
|
|
90
|
+
const created = await fetch(`${c.apiUrl}/api/coin/intents`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { ...authHeaders(), "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify({ skuId: opts.skuId, quantity: opts.quantity ?? 1 })
|
|
94
|
+
});
|
|
95
|
+
if (!created.ok) {
|
|
96
|
+
const body = await created.json().catch(() => ({}));
|
|
97
|
+
return { status: "failed", message: body.error ?? `intent failed (${created.status})` };
|
|
98
|
+
}
|
|
99
|
+
const view = await created.json();
|
|
100
|
+
const opened = openConfirmSurface(view);
|
|
101
|
+
if (!opened.ok) {
|
|
102
|
+
const message = opened.reason === "popup_blocked" ? "the confirmation window was blocked \u2014 tap again to confirm" : opened.reason === "native_unsupported" ? "purchases are not available in the app yet" : "the confirmation window could not be opened";
|
|
103
|
+
return { status: "failed", message };
|
|
104
|
+
}
|
|
105
|
+
return awaitOutcome(view.id, opts.timeoutMs ?? 18e4);
|
|
106
|
+
}
|
|
107
|
+
|
|
1
108
|
// ../embed-protocol/src/constants.ts
|
|
2
109
|
var NATIVE_CHANNEL = "genex-native";
|
|
3
110
|
var NATIVE_PROTOCOL_VERSION = 1;
|
|
@@ -234,7 +341,7 @@ var RETRY_FLAG = "genex:embed:retry";
|
|
|
234
341
|
var POPOVER_DISMISSED_FLAG = "genex:guest:popover-dismissed";
|
|
235
342
|
var LOCAL_AUTH_FLAG = "genex:embed:local-auth";
|
|
236
343
|
var PLAYER_ID_KEY = "genex:player";
|
|
237
|
-
var SDK_VERSION = "0.
|
|
344
|
+
var SDK_VERSION = "0.15.0";
|
|
238
345
|
var config = null;
|
|
239
346
|
var state = "pending";
|
|
240
347
|
var user = null;
|
|
@@ -337,17 +444,25 @@ function playerId() {
|
|
|
337
444
|
return "no-storage";
|
|
338
445
|
}
|
|
339
446
|
}
|
|
340
|
-
function initEmbed(
|
|
447
|
+
function initEmbed(cfg2) {
|
|
341
448
|
const w = win();
|
|
342
449
|
if (!w) return;
|
|
343
450
|
if (initialized) return;
|
|
344
451
|
initialized = true;
|
|
345
452
|
config = {
|
|
346
|
-
slug:
|
|
347
|
-
apiUrl:
|
|
348
|
-
dashboardOrigins: [...
|
|
453
|
+
slug: cfg2.slug,
|
|
454
|
+
apiUrl: cfg2.apiUrl.replace(/\/$/, ""),
|
|
455
|
+
dashboardOrigins: [...cfg2.dashboardOrigins]
|
|
349
456
|
};
|
|
350
457
|
state = "pending";
|
|
458
|
+
_initCommerce({
|
|
459
|
+
apiUrl: config.apiUrl,
|
|
460
|
+
dashboardOrigins: config.dashboardOrigins,
|
|
461
|
+
getToken: () => embedToken,
|
|
462
|
+
isEmbedded,
|
|
463
|
+
isNative: inNativeMode,
|
|
464
|
+
isLocalTest: () => localTestMode
|
|
465
|
+
});
|
|
351
466
|
if (wantsLocalTestMode(w)) {
|
|
352
467
|
enterLocalTestMode(w);
|
|
353
468
|
return;
|
|
@@ -1391,6 +1506,10 @@ function __resetForTests(overrides) {
|
|
|
1391
1506
|
}
|
|
1392
1507
|
|
|
1393
1508
|
export {
|
|
1509
|
+
getShop,
|
|
1510
|
+
getEntitlements,
|
|
1511
|
+
consumeEntitlement,
|
|
1512
|
+
buy,
|
|
1394
1513
|
initEmbed,
|
|
1395
1514
|
isEmbedded,
|
|
1396
1515
|
getAuthState,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,110 @@
|
|
|
1
|
+
/** A thing the game sells. Resolved server-side; the game never sets a price. */
|
|
2
|
+
interface ShopItem {
|
|
3
|
+
id: string;
|
|
4
|
+
/** 'consumable' — spent on use. 'durable' — owned permanently. */
|
|
5
|
+
type: string;
|
|
6
|
+
name: string;
|
|
7
|
+
iconUrl: string | null;
|
|
8
|
+
priceCoins: number;
|
|
9
|
+
/**
|
|
10
|
+
* The real-money equivalent, in USD cents, from the server.
|
|
11
|
+
*
|
|
12
|
+
* Show it next to the coin price. It is not decoration: a currency price
|
|
13
|
+
* without its real-world value is the practice consumer regulators single
|
|
14
|
+
* out first, and the platform sends this so a game never has to compute it —
|
|
15
|
+
* or be tempted to omit it.
|
|
16
|
+
*/
|
|
17
|
+
priceDisplayUsdCents: number;
|
|
18
|
+
}
|
|
19
|
+
interface Entitlement {
|
|
20
|
+
id: string;
|
|
21
|
+
skuId: string;
|
|
22
|
+
name: string;
|
|
23
|
+
type: string;
|
|
24
|
+
quantity: number;
|
|
25
|
+
consumed: boolean;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
}
|
|
28
|
+
type PurchaseStatus =
|
|
29
|
+
/** Settled. The item is owned; consume it, then apply the effect. */
|
|
30
|
+
'succeeded'
|
|
31
|
+
/** The player closed or declined the confirmation. Not an error — say nothing. */
|
|
32
|
+
| 'canceled'
|
|
33
|
+
/** The confirmation was never completed in time. */
|
|
34
|
+
| 'expired'
|
|
35
|
+
/** The player does not have enough coin. */
|
|
36
|
+
| 'insufficient_balance'
|
|
37
|
+
/** Something went wrong. `message` says what, for a log — not for the player. */
|
|
38
|
+
| 'failed';
|
|
39
|
+
interface PurchaseResult {
|
|
40
|
+
status: PurchaseStatus;
|
|
41
|
+
/** Present only on 'succeeded'. */
|
|
42
|
+
entitlementId?: string;
|
|
43
|
+
message?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The items this game sells.
|
|
47
|
+
*
|
|
48
|
+
* Render `name`, `iconUrl`, `priceCoins` AND `priceDisplayUsdCents` from here.
|
|
49
|
+
* Do not hardcode a price in the game: the server charges what its own catalog
|
|
50
|
+
* says, so a hardcoded one is a number that can silently disagree with what the
|
|
51
|
+
* player is actually charged.
|
|
52
|
+
*/
|
|
53
|
+
declare function getShop(): Promise<ShopItem[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Everything this player owns in this game.
|
|
56
|
+
*
|
|
57
|
+
* Call it on EVERY boot with `{ excludeConsumed: true }` and deliver whatever
|
|
58
|
+
* comes back. That is not an optimisation — it is how a purchase survives a
|
|
59
|
+
* crash between paying and receiving.
|
|
60
|
+
*/
|
|
61
|
+
declare function getEntitlements(opts?: {
|
|
62
|
+
excludeConsumed?: boolean;
|
|
63
|
+
}): Promise<Entitlement[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Mark an entitlement used, then apply its effect.
|
|
66
|
+
*
|
|
67
|
+
* That order matters and is worth stating plainly: consume FIRST, apply SECOND.
|
|
68
|
+
* If the game dies in between, the player loses one item — a support ticket. If
|
|
69
|
+
* you apply first and die before consuming, every boot re-delivers it forever —
|
|
70
|
+
* an exploit.
|
|
71
|
+
*
|
|
72
|
+
* Idempotent. `alreadyConsumed: true` means someone else got there first and
|
|
73
|
+
* you must NOT apply the effect again.
|
|
74
|
+
*/
|
|
75
|
+
declare function consumeEntitlement(entitlementId: string): Promise<{
|
|
76
|
+
consumed: boolean;
|
|
77
|
+
alreadyConsumed: boolean;
|
|
78
|
+
}>;
|
|
79
|
+
/**
|
|
80
|
+
* Buy something.
|
|
81
|
+
*
|
|
82
|
+
* **Call this synchronously from a real click or tap handler.** On a game's own
|
|
83
|
+
* origin the confirmation is a popup, and browsers only allow one while a user
|
|
84
|
+
* gesture is live — an `await` before this call loses that gesture and the
|
|
85
|
+
* purchase cannot open.
|
|
86
|
+
*
|
|
87
|
+
* Resolves once the SERVER says what happened. A closed window proves nothing:
|
|
88
|
+
* this waits for the ledger, not for the UI.
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* button.addEventListener('click', async () => {
|
|
92
|
+
* const result = await buy({ skuId: 'sku_potion' });
|
|
93
|
+
* if (result.status !== 'succeeded') return; // canceled is normal
|
|
94
|
+
* for (const e of await getEntitlements({ excludeConsumed: true })) {
|
|
95
|
+
* const { alreadyConsumed } = await consumeEntitlement(e.id);
|
|
96
|
+
* if (!alreadyConsumed) grantPotion(); // consume, THEN apply
|
|
97
|
+
* }
|
|
98
|
+
* });
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
declare function buy(opts: {
|
|
102
|
+
skuId: string;
|
|
103
|
+
quantity?: number;
|
|
104
|
+
/** How long to wait for the player to decide. Default 3 minutes. */
|
|
105
|
+
timeoutMs?: number;
|
|
106
|
+
}): Promise<PurchaseResult>;
|
|
107
|
+
|
|
1
108
|
interface EmbedConfig {
|
|
2
109
|
/** This game's own slug (GENEX.slug) — identifies the project to /play/authorize. */
|
|
3
110
|
slug: string;
|
|
@@ -232,4 +339,4 @@ declare function __resetForTests(overrides?: {
|
|
|
232
339
|
heartbeatIntervalMs?: number;
|
|
233
340
|
}): void;
|
|
234
341
|
|
|
235
|
-
export { type AuthState, type EmbedConfig, type EmbedEvent, type EmbedUser, type Leaderboard, type LeaderboardEntry, type PlayerStateResult, type SaveStateResult, type SubmitScoreResult, type WorldStateResult, __resetForTests, _stashTicketFromUrl, getAuthState, getColyseusAuth, getColyseusUrls, getEmbedToken, getLeaderboard, getUser, initEmbed, isEmbedded, loadPlayerState, loadWorldState, on, savePlayerState, saveWorldState, submitScore, waitForAuth, waitForPlayer };
|
|
342
|
+
export { type AuthState, type EmbedConfig, type EmbedEvent, type EmbedUser, type Entitlement, type Leaderboard, type LeaderboardEntry, type PlayerStateResult, type PurchaseResult, type PurchaseStatus, type SaveStateResult, type ShopItem, type SubmitScoreResult, type WorldStateResult, __resetForTests, _stashTicketFromUrl, buy, consumeEntitlement, getAuthState, getColyseusAuth, getColyseusUrls, getEmbedToken, getEntitlements, getLeaderboard, getShop, getUser, initEmbed, isEmbedded, loadPlayerState, loadWorldState, on, savePlayerState, saveWorldState, submitScore, waitForAuth, waitForPlayer };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
__resetForTests,
|
|
3
3
|
_stashTicketFromUrl,
|
|
4
|
+
buy,
|
|
5
|
+
consumeEntitlement,
|
|
4
6
|
getAuthState,
|
|
5
7
|
getColyseusAuth,
|
|
6
8
|
getColyseusUrls,
|
|
7
9
|
getEmbedToken,
|
|
10
|
+
getEntitlements,
|
|
8
11
|
getLeaderboard,
|
|
12
|
+
getShop,
|
|
9
13
|
getUser,
|
|
10
14
|
initEmbed,
|
|
11
15
|
isEmbedded,
|
|
@@ -17,15 +21,19 @@ import {
|
|
|
17
21
|
submitScore,
|
|
18
22
|
waitForAuth,
|
|
19
23
|
waitForPlayer
|
|
20
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-DWDBRKRS.js";
|
|
21
25
|
export {
|
|
22
26
|
__resetForTests,
|
|
23
27
|
_stashTicketFromUrl,
|
|
28
|
+
buy,
|
|
29
|
+
consumeEntitlement,
|
|
24
30
|
getAuthState,
|
|
25
31
|
getColyseusAuth,
|
|
26
32
|
getColyseusUrls,
|
|
27
33
|
getEmbedToken,
|
|
34
|
+
getEntitlements,
|
|
28
35
|
getLeaderboard,
|
|
36
|
+
getShop,
|
|
29
37
|
getUser,
|
|
30
38
|
initEmbed,
|
|
31
39
|
isEmbedded,
|
package/dist/sentry.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/embed-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Player identity + durable game state for genex games \u2014 signed-in or guest play, per-player save slots, shared world state, and soft-trust leaderboards.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|