@playdotfun/game-sdk 1.0.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.
@@ -0,0 +1,50 @@
1
+ import { SDKEvents } from '@/types';
2
+
3
+ export default (eventHandler: EventTarget) => {
4
+ function emit(evt: SDKEvents | '*', data?: Record<string, any>) {
5
+ const cEvent = new CustomEvent(evt, { detail: data });
6
+ console.debug(`[OpenGameSDK] Dispatching event: ${evt} to SDK handler`);
7
+ eventHandler.dispatchEvent(cEvent);
8
+
9
+ if (window) {
10
+ console.debug(`[OpenGameSDK] Dispatching event: ${evt} to window`);
11
+ window.dispatchEvent(cEvent);
12
+ }
13
+
14
+ const globalEvent = new CustomEvent('*', {
15
+ detail: {
16
+ name: evt,
17
+ data,
18
+ },
19
+ });
20
+
21
+ console.debug(`[OpenGameSDK] Dispatching global event: ${globalEvent.detail} to SDK handler`);
22
+ eventHandler.dispatchEvent(globalEvent);
23
+ if (window) {
24
+ console.debug('[OpenGameSDK] Dispatching global event:', globalEvent.detail, 'to window');
25
+ window.dispatchEvent(globalEvent);
26
+ }
27
+ }
28
+ function on(event: SDKEvents, callback: (event: CustomEvent) => void) {
29
+ console.debug('[OpenGameSDK] Adding event listener for event:', event);
30
+ eventHandler.addEventListener(event, (e: Event) => callback(e as CustomEvent));
31
+ }
32
+ function off(event: SDKEvents | '*', callback: (event: CustomEvent) => void) {
33
+ console.debug('[OpenGameSDK] Removing event listener for event:', event);
34
+ eventHandler.removeEventListener(event, callback as EventListener);
35
+ }
36
+ function onAll(callback: (event: SDKEvents, data?: Record<string, any>) => void) {
37
+ console.debug('[OpenGameSDK] Adding event listener for all events');
38
+ eventHandler.addEventListener('*', (e: Event) => {
39
+ const { name, data } = (e as CustomEvent).detail;
40
+ callback(name as SDKEvents, data);
41
+ });
42
+ }
43
+
44
+ return {
45
+ emit,
46
+ on,
47
+ off,
48
+ onAll,
49
+ };
50
+ };
@@ -0,0 +1,74 @@
1
+ const k7x = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
2
+ function m9q(s: string) {
3
+ let h = 5381;
4
+ for (let i = 0; i < s.length; i++) h = (h * 33) ^ s.charCodeAt(i);
5
+ return h >>> 0;
6
+ }
7
+ function w3r(n: number) {
8
+ let x = n || 2463534242;
9
+ return () => {
10
+ x ^= x << 13;
11
+ x ^= x >>> 17;
12
+ x ^= x << 5;
13
+ return (x >>> 0) / 0x100000000;
14
+ };
15
+ }
16
+ function p8t(k: string) {
17
+ const a = k7x.split('');
18
+ const r = w3r(m9q(k));
19
+ for (let i = a.length - 1; i > 0; i--) {
20
+ const j = Math.floor(r() * (i + 1));
21
+ [a[i], a[j]] = [a[j], a[i]];
22
+ }
23
+ return a.join('');
24
+ }
25
+ function l5n(s: string, f: string, t: string) {
26
+ const m = new Map<string, string>();
27
+ for (let i = 0; i < f.length; i++) m.set(f[i], t[i]);
28
+ let o = '';
29
+ for (const c of s) o += m.get(c) ?? c;
30
+ return o;
31
+ }
32
+ function d2v(s: string, z = 6, p = '-') {
33
+ const x = [];
34
+ for (let i = 0; i < s.length; i += z) x.push(s.slice(i, i + z));
35
+ return x.join(p);
36
+ }
37
+ function g6h(s: string, p = '-') {
38
+ return s.split(p).join('');
39
+ }
40
+ function q1u(b: Buffer, k: Buffer) {
41
+ const o = Buffer.allocUnsafe(b.length);
42
+ for (let i = 0; i < b.length; i++) o[i] = b[i] ^ k[i % k.length];
43
+ return o;
44
+ }
45
+ export function encodePointsObf(points: number, key = 'default_obf_key') {
46
+ if (!Number.isFinite(points)) throw new Error('points must be finite number');
47
+ const y = `${points}:${Date.now()}`;
48
+ const yb = Buffer.from(y, 'utf8');
49
+ const kb = Buffer.from(key, 'utf8');
50
+ const x = q1u(yb, kb);
51
+ const b = x.toString('base64');
52
+ const s = p8t(key);
53
+ const m = l5n(b, k7x, s);
54
+ const t = d2v(m.split('').reverse().join(''), 6, '-');
55
+ return t;
56
+ }
57
+ export function decodePointsObf(token: string, key = 'default_obf_key') {
58
+ try {
59
+ const c = g6h(token, '-');
60
+ const u = c.split('').reverse().join('');
61
+ const s = p8t(key);
62
+ const b = l5n(u, s, k7x);
63
+ const x = Buffer.from(b, 'base64');
64
+ const kb = Buffer.from(key, 'utf8');
65
+ const yb = q1u(x, kb);
66
+ const y = yb.toString('utf8');
67
+ const [v] = y.split(':');
68
+ const n = Number(v);
69
+ if (!Number.isFinite(n)) throw new Error('invalid payload');
70
+ return n;
71
+ } catch (e) {
72
+ throw new Error('Failed to decode obfuscated token');
73
+ }
74
+ }
package/src/types.ts ADDED
@@ -0,0 +1,67 @@
1
+ export type {
2
+ SDKOpts,
3
+ SDKState,
4
+ Token,
5
+ Game,
6
+ Theme,
7
+ Session,
8
+ Context,
9
+ ListUserRewardsResponse,
10
+ ClaimRewardsResponse,
11
+ PromiseResolveFn,
12
+ User,
13
+ } from '@play-fun/types/sdk';
14
+
15
+ export {
16
+ WidgetScreen,
17
+ SDKMessages,
18
+ CarouselMessages,
19
+ WidgetMessages,
20
+ SDKEvents,
21
+ SDKActions,
22
+ WidgetActions,
23
+ CarouselActions,
24
+ } from '@play-fun/types/sdk';
25
+
26
+ export interface PaginatedFetchOpts {
27
+ limit?: number;
28
+ cursor?: string;
29
+ sort?: string;
30
+ sortBy?: string;
31
+ query?: string;
32
+ }
33
+
34
+ export type RecursivePartial<T> = {
35
+ [P in keyof T]?: RecursivePartial<T[P]>;
36
+ };
37
+
38
+ // Flush protocol types
39
+ export interface FlushState {
40
+ seq: number;
41
+ currentHash: string;
42
+ stepKey: string;
43
+ totalPoints: number;
44
+ initialized: boolean;
45
+ }
46
+
47
+ export interface FlushResponse {
48
+ seq: number;
49
+ newHash: string;
50
+ nextStepKey: string;
51
+ total: number;
52
+ added: number;
53
+ duplicate?: boolean;
54
+ }
55
+
56
+ export interface CommitResponse {
57
+ saved: number;
58
+ status: 'accepted' | 'pending_review';
59
+ riskScore?: number;
60
+ }
61
+
62
+ export interface SessionStateResponse {
63
+ lastSeq: number;
64
+ lastHash: string;
65
+ stepKey: string;
66
+ totalPoints: number;
67
+ }
@@ -0,0 +1,157 @@
1
+ import config, { isDevOrigin } from '@/config';
2
+ import {
3
+ PromiseResolveFn,
4
+ SDKActions,
5
+ SDKMessages,
6
+ Theme,
7
+ WidgetActions,
8
+ WidgetMessages,
9
+ } from '@/types';
10
+ import { UpwardsDashboardActions, UpwardDashboardMessages } from '@play-fun/types/sdk';
11
+ import { DashboardBridge } from '@/dashboard/bridge';
12
+
13
+ const sandbox = `
14
+ allow-same-origin
15
+ allow-scripts
16
+ allow-forms
17
+ allow-popups
18
+ allow-modals
19
+ allow-downloads
20
+ allow-top-navigation
21
+ allow-top-navigation-by-user-activation
22
+ `;
23
+
24
+ const initialStyle = {
25
+ display: 'none',
26
+ width: '100%',
27
+ height: '100%',
28
+ position: 'fixed',
29
+ zIndex: '9999',
30
+ background: 'transparent',
31
+ padding: '0',
32
+ margin: '0',
33
+ top: '0',
34
+ left: '0',
35
+ border: 'none',
36
+ };
37
+
38
+ export class WidgetBridge {
39
+ private style: Partial<CSSStyleDeclaration> = { ...initialStyle };
40
+
41
+ private widgetIFrame: HTMLIFrameElement;
42
+
43
+ private widgetElementResolver?: PromiseResolveFn;
44
+ private widgetElementPromise = new Promise((resolve) => (this.widgetElementResolver = resolve));
45
+
46
+ private widgetReadyResolver?: PromiseResolveFn;
47
+ private widgetReadyPromise = new Promise((resolve) => (this.widgetReadyResolver = resolve));
48
+
49
+ private widgetReady = false;
50
+
51
+ constructor(
52
+ private handleWidgetMessage: (msg: WidgetMessages) => void,
53
+ private url = config.widgetURL,
54
+ ) {
55
+ window.addEventListener('message', (e) => {
56
+ const originHostname = new URL(e.origin).hostname;
57
+ const isValidOrigin =
58
+ e.origin === config.widgetURL ||
59
+ (config.dashboardURL && e.origin === config.dashboardURL) ||
60
+ isDevOrigin(originHostname);
61
+ if (isValidOrigin) {
62
+ this._handleWidgetActions(e.data);
63
+ }
64
+ });
65
+
66
+ this.widgetIFrame = document.createElement('iframe') as HTMLIFrameElement;
67
+ this.widgetIFrame.sandbox.value = sandbox;
68
+ this._updateStyle(initialStyle);
69
+ }
70
+
71
+ async init(target: HTMLElement = document.body, url?: string) {
72
+ this.widgetIFrame.src = url ?? this.url;
73
+ this.widgetIFrame.onload = () => {
74
+ this.widgetElementResolver?.({});
75
+ };
76
+ this._waitForWidgetReady();
77
+ target.appendChild(this.widgetIFrame);
78
+ await Promise.all([this.widgetElementPromise, this.widgetReadyPromise]);
79
+ }
80
+
81
+ sendMessage(msg: SDKMessages) {
82
+ if (this.widgetIFrame.contentWindow) {
83
+ console.log(`Sending message to widget: ${msg.action}`);
84
+ const targetOrigin = new URL(this.url).origin;
85
+ this.widgetIFrame.contentWindow.postMessage(msg, targetOrigin);
86
+ } else {
87
+ console.warn(`No contentWindow found for widget iframe. Message not sent: ${msg.action}`);
88
+ }
89
+ }
90
+
91
+ show(extraStyle?: Partial<CSSStyleDeclaration>) {
92
+ this._updateStyle({
93
+ display: 'block',
94
+ ...extraStyle,
95
+ });
96
+ }
97
+
98
+ hide() {
99
+ this._updateStyle({ ...initialStyle });
100
+ }
101
+
102
+ setTheme(theme: Theme) {
103
+ this.sendMessage({
104
+ action: SDKActions.UI_SetTheme,
105
+ data: theme,
106
+ });
107
+ }
108
+
109
+ private _updateStyle = (style: Partial<CSSStyleDeclaration>) => {
110
+ this.style = {
111
+ ...this.style,
112
+ ...style,
113
+ };
114
+ Object.keys(style).forEach((styleKey) => {
115
+ // @ts-ignore
116
+ this.widgetIFrame.style[styleKey] = style[styleKey];
117
+ });
118
+ };
119
+
120
+ private _handleWidgetActions(msg: WidgetMessages | UpwardDashboardMessages) {
121
+ console.log('Handling Widget Action:', msg.action);
122
+
123
+ // Check if this is an UpwardsDashboardAction that should be forwarded to the dashboard
124
+ const isUpwardsDashboardAction = Object.values(UpwardsDashboardActions).includes(
125
+ msg.action as UpwardsDashboardActions,
126
+ );
127
+ if (isUpwardsDashboardAction) {
128
+ console.log('Forwarding UpwardsDashboardAction to dashboard:', msg.action);
129
+ DashboardBridge.sendMessage(msg as UpwardDashboardMessages);
130
+ return;
131
+ }
132
+
133
+ const isValidAction = Object.values(WidgetActions).includes(msg.action as WidgetActions);
134
+ if (isValidAction) {
135
+ if (msg.action === WidgetActions.WidgetReady) {
136
+ console.log('Widget is ready!');
137
+ this.widgetReady = true;
138
+ this.widgetReadyResolver?.({});
139
+ } else {
140
+ this.handleWidgetMessage(msg as WidgetMessages);
141
+ }
142
+ }
143
+ }
144
+
145
+ private _waitForWidgetReady() {
146
+ if (this.widgetReady) {
147
+ return;
148
+ }
149
+ this.sendMessage({
150
+ action: SDKActions.CheckForWidgetReady,
151
+ data: undefined,
152
+ });
153
+ setTimeout(() => {
154
+ this._waitForWidgetReady();
155
+ }, 250);
156
+ }
157
+ }
@@ -0,0 +1,45 @@
1
+ import OpenGameSDK from '@/sdk';
2
+ import { SDKActions, WidgetActions, WidgetMessages } from '@/types';
3
+ import { DashboardActions, DashboardMessages } from '@play-fun/types/sdk';
4
+
5
+ export const handleWidgetMessage = async (self: OpenGameSDK, msg: WidgetMessages | DashboardMessages) => {
6
+ console.log(`Received widget message: ${msg.action}`, msg.data);
7
+
8
+ switch (msg.action) {
9
+ case WidgetActions.SetPrivyToken:
10
+ console.log('[WidgetBridge] Received Privy token');
11
+ self.handlePrivyTokenResponse(msg.data.privyAccessToken, msg.data.requestId);
12
+ return;
13
+ case WidgetActions.EnableView:
14
+ const heightValue =
15
+ typeof msg.data?.height === 'number'
16
+ ? `${msg.data.height}px`
17
+ : (msg.data?.height ?? '100%');
18
+ return self.widget?.show({
19
+ height: heightValue,
20
+ maxHeight: heightValue,
21
+ });
22
+ case WidgetActions.DisableView:
23
+ self.widget?.hide();
24
+ if (self.pointsDisplay?.show) {
25
+ setTimeout(() => {
26
+ self.showPoints();
27
+ });
28
+ }
29
+ break;
30
+ case WidgetActions.SetIdentityToken:
31
+ return self.setIdentityToken(msg.data);
32
+
33
+ case WidgetActions.SetPlayerId:
34
+ console.log('[WidgetBridge] Setting player ID:', msg.data);
35
+ if (!self.gameId) {
36
+ throw new Error('No game ID found. Cannot set player ID.');
37
+ }
38
+ const { playerId, privyAccessToken } = msg.data;
39
+ // Don't set playerId here - loadGame will set it from server response
40
+ // Setting it before loadGame causes the early-return check to trigger
41
+ return self.loadGame(self.gameId, playerId, privyAccessToken);
42
+ case WidgetActions.ShowClaim:
43
+ return self.showClaim();
44
+ }
45
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "module": "es2022",
5
+ "moduleResolution": "node",
6
+ "emitDecoratorMetadata": true,
7
+ "experimentalDecorators": true,
8
+ "declaration": true,
9
+ "rootDir": "./src",
10
+ "outDir": "./dist",
11
+ "baseUrl": ".",
12
+ "paths": {
13
+ "@/*": [
14
+ "./src/*"
15
+ ]
16
+ },
17
+ "lib": ["DOM", "ES2022"],
18
+ "strict": true,
19
+ "esModuleInterop": true,
20
+ "skipLibCheck": false,
21
+ "forceConsistentCasingInFileNames": true,
22
+ },
23
+ "include": ["./src/**/*",],
24
+ "exclude": ["node_modules", "dist", "*.config.ts"]
25
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,77 @@
1
+ import { defineConfig } from 'tsup';
2
+ import { config } from 'dotenv';
3
+ config();
4
+
5
+ export default defineConfig([
6
+ {
7
+ entry: ['src/index.ts'],
8
+ format: ['esm', 'cjs'],
9
+ dts: true,
10
+ splitting: false,
11
+ sourcemap: true,
12
+ clean: true,
13
+ minify: false,
14
+ treeshake: true,
15
+ target: 'es2022',
16
+ outDir: 'dist',
17
+ platform: 'node',
18
+ esbuildOptions(options) {
19
+ options.define = {
20
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
21
+ 'process.env.OGP_API_URL': JSON.stringify(
22
+ process.env.OGP_API_URL || 'https://api.play.fun',
23
+ ),
24
+ 'process.env.OGP_DASHBOARD_WIDGET_URL': JSON.stringify(
25
+ process.env.OGP_DASHBOARD_WIDGET_URL || '',
26
+ ),
27
+ 'process.env.OGP_CAROUSEL_URL': JSON.stringify(
28
+ process.env.OGP_CAROUSEL_URL || 'https://carousel.play.fun',
29
+ ),
30
+ 'process.env.OGP_WIDGET_URL': JSON.stringify(
31
+ process.env.OGP_WIDGET_URL || 'https://widget.play.fun',
32
+ ),
33
+ };
34
+ },
35
+ },
36
+ {
37
+ entry: { sdk: 'src/index.ts' },
38
+ format: ['iife'],
39
+ globalName: 'OpenGameSDK',
40
+ outDir: 'dist',
41
+ outExtension: () => ({ js: '.js' }),
42
+ platform: 'browser',
43
+ target: 'es2020',
44
+ clean: true,
45
+ dts: true,
46
+ sourcemap: true,
47
+ bundle: true,
48
+ minify: false,
49
+ inject: ['src/buffer-shim.ts'],
50
+ noExternal: ['@play-fun/types'],
51
+ esbuildOptions(options) {
52
+ options.alias = {
53
+ '@': './src',
54
+ };
55
+ options.define = {
56
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
57
+ 'process.env.OGP_API_URL': JSON.stringify(
58
+ process.env.OGP_API_URL || 'https://api.play.fun',
59
+ ),
60
+ 'process.env.OGP_DASHBOARD_URL': JSON.stringify(process.env.OGP_DASHBOARD_URL || ''),
61
+ 'process.env.OGP_CAROUSEL_URL': JSON.stringify(
62
+ process.env.OGP_CAROUSEL_URL || 'https://carousel.play.fun',
63
+ ),
64
+ 'process.env.OGP_WIDGET_URL': JSON.stringify(
65
+ process.env.OGP_WIDGET_URL || 'https://widget.play.fun',
66
+ ),
67
+ };
68
+ options.footer = {
69
+ js: 'OpenGameSDK = OpenGameSDK.default;',
70
+ };
71
+ // options.pure =
72
+ // process.env.NODE_ENV === 'production'
73
+ // ? ['console.log', 'console.debug', 'console.info']
74
+ // : [];
75
+ },
76
+ },
77
+ ]);