@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.
- package/DEVELOPERS.md +94 -0
- package/index.html +219 -0
- package/package.json +48 -0
- package/src/buffer-shim.ts +3 -0
- package/src/carousel/messages.ts +145 -0
- package/src/config.ts +26 -0
- package/src/dashboard/bridge.ts +8 -0
- package/src/http/client.ts +58 -0
- package/src/index.ts +3 -0
- package/src/scripts/deploy.ts +135 -0
- package/src/sdk/crypto.ts +51 -0
- package/src/sdk/index.ts +1488 -0
- package/src/sdk/messaging.ts +50 -0
- package/src/sdk/obfuscation.ts +74 -0
- package/src/types.ts +67 -0
- package/src/widget/bridge.ts +157 -0
- package/src/widget/messages.ts +45 -0
- package/tsconfig.json +25 -0
- package/tsup.config.ts +77 -0
package/DEVELOPERS.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# How to run the SDK locally
|
|
2
|
+
|
|
3
|
+
## Environment
|
|
4
|
+
|
|
5
|
+
### In `sdks/vanilla-js/.env`
|
|
6
|
+
|
|
7
|
+
- Set OGP_WIDGET_URL to `https://<yourname>-widget.ngrok.io`
|
|
8
|
+
- Set OGP_DASHBOARD_URL to `https://<yourname>-privy.ngrok.io`
|
|
9
|
+
|
|
10
|
+
### In `apps/frontend/.env`
|
|
11
|
+
|
|
12
|
+
- Set NEXT_PUBLIC_WIDGET_URL to `https://<yourname>-widget.ngrok.io`
|
|
13
|
+
|
|
14
|
+
## In root `.env`
|
|
15
|
+
|
|
16
|
+
- Set VITE_API_URL to `https://<yourname>-api.ngrok.io`
|
|
17
|
+
|
|
18
|
+
## Example HTML File Changes
|
|
19
|
+
|
|
20
|
+
In `sdks/vanilla-js/index.html`, change this line 6 to match your user ID from your local DB:
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<meta name="x-ogp-key" content="b6f44650-fd5b-49ee-b188-dc2224ed406d" id="ogp-key-meta" />
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
On the line that says:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const gameId = params.get('gameId') ?? '...';
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Change the `...` to your targeted game ID, or provider ?gameId=xxxx in the URL.
|
|
33
|
+
|
|
34
|
+
Look for `sdkConfig` and replace the `baseUrl` with your API url, you may not need to change this:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
const sdkConfig = {
|
|
38
|
+
apiKey: finalApiKey,
|
|
39
|
+
gameId,
|
|
40
|
+
playerId,
|
|
41
|
+
ui: {
|
|
42
|
+
usePointsWidget: true,
|
|
43
|
+
},
|
|
44
|
+
logLevel: 0,
|
|
45
|
+
baseUrl: 'http://localhost:4000', // <-- Change this to your API url
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Running the test suite and frontend
|
|
50
|
+
|
|
51
|
+
1. Run `pnpm i` in the root directory
|
|
52
|
+
|
|
53
|
+
2. In 3 separate terminals, run the following in order and wait for each to complete before moving on to the next:
|
|
54
|
+
|
|
55
|
+
- Run `pnpm sdk-suite` to start the local SDK suite
|
|
56
|
+
- Run `pnpm widget:dev` to start the local widget
|
|
57
|
+
- Run `pnpm frontend` to start the local frontend
|
|
58
|
+
|
|
59
|
+
3. Start an NGROK tunnel for your SDK suite: `ngrok http --domain=<yourname>-demo.ngrok.io 3000`
|
|
60
|
+
4. Start an NGROK tunnel for your widget: `ngrok http --domain=<yourname>-widget.ngrok.io 3001`
|
|
61
|
+
5. Start an NGROK tunnel for your frontend: `ngrok http --domain=<yourname>-privy.ngrok.io 3002`
|
|
62
|
+
|
|
63
|
+
6. Visit `https://<yourname>-demo.ngrok.io` to see the test suite directly, or visit your game in the dashboard with your demo ngrok URL as the game URL to test within the dashboard.
|
|
64
|
+
|
|
65
|
+
# Env and Config Cheatsheet
|
|
66
|
+
|
|
67
|
+
| Env Variable | Description | File Path | Local Value | Dev Value | Staging Value | Prod Value |
|
|
68
|
+
| :--------------------- | :---------- | :--------------------- | :----------------------------------- | :-------- | :------------ | :--------- |
|
|
69
|
+
| NEXT_PUBLIC_WIDGET_URL | Description | `apps/frontend/.env` | `https://<yourname>-widget.ngrok.io` | TODO | TODO | TODO |
|
|
70
|
+
| OGP_WIDGET_URL | Description | `sdks/vanilla-js/.env` | `https://<yourname>-widget.ngrok.io` | TODO | TODO | TODO |
|
|
71
|
+
| OGP_DASHBOARD_URL | Description | `sdks/vanilla-js/.env` | `https://<yourname>-privy.ngrok.io` | TODO | TODO | TODO |
|
|
72
|
+
| VITE_API_URL | Description | `.env` | `https://localhost:4000` | TODO | TODO | TODO |
|
|
73
|
+
|
|
74
|
+
## Before saying something is broken do the following:
|
|
75
|
+
|
|
76
|
+
1. What environment am I targeting?
|
|
77
|
+
|
|
78
|
+
2. Have I set my corrent environment variables based on the environment I'm targeting?
|
|
79
|
+
|
|
80
|
+
3. Have I properly changed the apiKey in the example HTML file based on the environment I'm targeting?
|
|
81
|
+
|
|
82
|
+
4. Have I changed the baseURL in the SDK config based on the environment I'm targeting?
|
|
83
|
+
|
|
84
|
+
5. Have I changed the gameId in the SDK initialization based on the environment I'm targeting? I can easily change this by passing the gameId as a query parameter to the URL.
|
|
85
|
+
|
|
86
|
+
# How to deploy
|
|
87
|
+
|
|
88
|
+
Make sure you bump the version in `sdks/vanilla-js/package.json` to the new version you want to deploy.
|
|
89
|
+
|
|
90
|
+
1. Run `pnpm build` in the `sdks/vanilla-js` directory
|
|
91
|
+
|
|
92
|
+
2. Run `pnpm deploy:cdn:<target>` in the root directory, where `<target>` is one of `dev`, `staging`, or `prod`
|
|
93
|
+
|
|
94
|
+
3. Run `npm publish --access public` in the `sdks/vanilla-js` directory
|
package/index.html
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
7
|
+
<meta name="x-ogp-key" content="b56bfa12-e2ca-4a93-bdae-e407a90a64f5" id="ogp-key-meta" />
|
|
8
|
+
<script src="./dist/sdk.js" id="playfun-sdk"></script>
|
|
9
|
+
<title>Click Counter</title>
|
|
10
|
+
<style>
|
|
11
|
+
body {
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
height: 100vh;
|
|
16
|
+
margin: 0;
|
|
17
|
+
font-family: Arial, sans-serif;
|
|
18
|
+
color: white;
|
|
19
|
+
background-color: rgb(39, 39, 39);
|
|
20
|
+
/* #f7f7f7; */
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
/* Prevent scrolling */
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Centered content container */
|
|
26
|
+
.content {
|
|
27
|
+
text-align: center;
|
|
28
|
+
width: 100%;
|
|
29
|
+
max-width: 375px;
|
|
30
|
+
/* Mobile-sized width */
|
|
31
|
+
padding: 20px;
|
|
32
|
+
box-sizing: border-box;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Styling for the counter */
|
|
36
|
+
.counter {
|
|
37
|
+
font-size: 24px;
|
|
38
|
+
margin-bottom: 10px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Styling for the clickable button */
|
|
42
|
+
button {
|
|
43
|
+
padding: 8px 16px;
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
border: none;
|
|
47
|
+
border-radius: 5px;
|
|
48
|
+
background-color: #007bff;
|
|
49
|
+
color: white;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
button:hover {
|
|
53
|
+
background-color: #0056b3;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
button:disabled {
|
|
57
|
+
background-color: #cccccc;
|
|
58
|
+
opacity: 0.8;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.auth-button {
|
|
62
|
+
position: fixed;
|
|
63
|
+
top: 10px;
|
|
64
|
+
right: 10px;
|
|
65
|
+
z-index: 1000;
|
|
66
|
+
padding: 8px 16px;
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
background-color: #28a745;
|
|
69
|
+
border: none;
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
color: white;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
font-weight: bold;
|
|
74
|
+
display: none;
|
|
75
|
+
/* Initially hidden until Privy is ready */
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.auth-button:hover {
|
|
79
|
+
background-color: #218838;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.auth-button.logout {
|
|
83
|
+
background-color: #dc3545;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.auth-button.logout:hover {
|
|
87
|
+
background-color: #c82333;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Optional: For mobile responsiveness */
|
|
91
|
+
@media (max-width: 375px) {
|
|
92
|
+
.content {
|
|
93
|
+
width: 100%;
|
|
94
|
+
padding: 10px;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@media (min-width: 376px) {
|
|
99
|
+
.content {
|
|
100
|
+
width: 375px;
|
|
101
|
+
/* This ensures content is capped at a mobile size */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
</head>
|
|
106
|
+
|
|
107
|
+
<body>
|
|
108
|
+
<div class="content">
|
|
109
|
+
<div class="counter" id="counter">0</div>
|
|
110
|
+
|
|
111
|
+
<button id="add-points-btn" disabled>Add Points</button>
|
|
112
|
+
<button id="decr-points-btn" disabled>Decr. Points</button>
|
|
113
|
+
<button id="save-points-btn" disabled>Save Points</button>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<script>
|
|
117
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
118
|
+
const params = new URLSearchParams(window.location.search);
|
|
119
|
+
const sdkOrigin = params.get('sdkOrigin');
|
|
120
|
+
const playerId = params.get('playerId');
|
|
121
|
+
const gameId = params.get('gameId') ?? '8222f50f-a794-45e6-b4b0-7446a8aa102e';
|
|
122
|
+
|
|
123
|
+
if (!gameId) {
|
|
124
|
+
console.error('Missing Game ID in query parameter.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const counterEl = document.getElementById('counter');
|
|
129
|
+
const addPointsBtn = document.getElementById('add-points-btn');
|
|
130
|
+
const decrPointsBtn = document.getElementById('decr-points-btn');
|
|
131
|
+
const savePointsBtn = document.getElementById('save-points-btn');
|
|
132
|
+
|
|
133
|
+
// Track if an operation is in progress to prevent double-clicks
|
|
134
|
+
let isProcessing = false;
|
|
135
|
+
|
|
136
|
+
console.log('Going to load the OpenGameSDK');
|
|
137
|
+
|
|
138
|
+
const sdkConfig = {
|
|
139
|
+
gameId,
|
|
140
|
+
playerId,
|
|
141
|
+
ui: {
|
|
142
|
+
usePointsWidget: true,
|
|
143
|
+
},
|
|
144
|
+
logLevel: 0,
|
|
145
|
+
baseUrl: 'http://localhost:4000',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const sdk = new OpenGameSDK(sdkConfig);
|
|
149
|
+
|
|
150
|
+
// Update display from SDK state to keep in sync
|
|
151
|
+
const updateDisplay = () => {
|
|
152
|
+
counterEl.innerText = sdk.sessionPoints.toString();
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
addPointsBtn.addEventListener('click', () => {
|
|
156
|
+
if (isProcessing) return;
|
|
157
|
+
isProcessing = true;
|
|
158
|
+
addPointsBtn.disabled = true;
|
|
159
|
+
|
|
160
|
+
sdk.addPoints(1);
|
|
161
|
+
updateDisplay();
|
|
162
|
+
|
|
163
|
+
// Re-enable after a short delay to prevent rapid double-clicks
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
isProcessing = false;
|
|
166
|
+
addPointsBtn.disabled = false;
|
|
167
|
+
}, 50);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
decrPointsBtn.addEventListener('click', () => {
|
|
171
|
+
if (isProcessing) return;
|
|
172
|
+
isProcessing = true;
|
|
173
|
+
decrPointsBtn.disabled = true;
|
|
174
|
+
|
|
175
|
+
sdk.decrPoints(1);
|
|
176
|
+
updateDisplay();
|
|
177
|
+
|
|
178
|
+
// Re-enable after a short delay to prevent rapid double-clicks
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
isProcessing = false;
|
|
181
|
+
decrPointsBtn.disabled = false;
|
|
182
|
+
}, 50);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
savePointsBtn.addEventListener('click', async () => {
|
|
186
|
+
if (isProcessing) return;
|
|
187
|
+
isProcessing = true;
|
|
188
|
+
savePointsBtn.disabled = true;
|
|
189
|
+
addPointsBtn.disabled = true;
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
await sdk.savePoints();
|
|
193
|
+
updateDisplay();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Error saving points:', error);
|
|
196
|
+
} finally {
|
|
197
|
+
isProcessing = false;
|
|
198
|
+
savePointsBtn.disabled = false;
|
|
199
|
+
addPointsBtn.disabled = false;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
sdk.on('OnReady', () => {
|
|
204
|
+
console.log('OpenGameSDK loaded and ready');
|
|
205
|
+
addPointsBtn.removeAttribute('disabled');
|
|
206
|
+
savePointsBtn.removeAttribute('disabled');
|
|
207
|
+
decrPointsBtn.removeAttribute('disabled');
|
|
208
|
+
updateDisplay();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
sdk
|
|
212
|
+
.init()
|
|
213
|
+
.then(() => console.log('OpenGameSDK Loaded'))
|
|
214
|
+
.catch(console.error);
|
|
215
|
+
});
|
|
216
|
+
</script>
|
|
217
|
+
</body>
|
|
218
|
+
|
|
219
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@playdotfun/game-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official SDK for interacting with the Play.fun API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.cjs",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
"*": {
|
|
14
|
+
"browser": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@play-fun/types": "workspace:^",
|
|
26
|
+
"@types/node": "^25.0.0",
|
|
27
|
+
"aws-sdk": "^2.1693.0",
|
|
28
|
+
"buffer": "^6.0.3",
|
|
29
|
+
"concurrently": "^9.2.1",
|
|
30
|
+
"esbuild-plugin-polyfill-node": "^0.3.0",
|
|
31
|
+
"process": "^0.11.10",
|
|
32
|
+
"tsup": "^8.5.1",
|
|
33
|
+
"tsx": "^4.21.0",
|
|
34
|
+
"typeorm": "^0.3.28",
|
|
35
|
+
"typescript": "catalog:"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"build:watch": "tsup --watch",
|
|
40
|
+
"sdk-suite": "pnpm dlx serve -l 3001",
|
|
41
|
+
"build:publish": "NODE_ENV=production tsup --clean --minify && npm publish --access public",
|
|
42
|
+
"deploy:cdn:dev-devnet": "OGP_WIDGET_URL=https://widget-iframe-dev-devnet.up.railway.app OGP_DASHBOARD_URL=https://frontend-dev-devnet.up.railway.app NODE_ENV=development tsup --minify --clean && tsx ./src/scripts/deploy.ts --target dev",
|
|
43
|
+
"deploy:cdn:dev-mainnet": "OGP_API_URL=https://dev.api.play.fun OGP_WIDGET_URL=https://widget.dev.play.fun OGP_DASHBOARD_URL=https://dev.play.fun OGP_CAROUSEL_URL=https://carousel.dev.play.fun NODE_ENV=development tsup --minify --clean && tsx ./src/scripts/deploy.ts --target dev-mainnet",
|
|
44
|
+
"deploy:cdn:prod": "OGP_API_URL=https://api.play.fun OGP_WIDGET_URL=https://widget.play.fun OGP_DASHBOARD_URL=https://play.fun NODE_ENV=production tsup --minify --clean && tsx ./src/scripts/deploy.ts",
|
|
45
|
+
"deploy:cdn:prod-staging": "OGP_WIDGET_URL=https://widget.play.fun OGP_DASHBOARD_URL=https://play.fun NODE_ENV=production tsup --minify --clean && tsx ./src/scripts/deploy.ts --target prod --latest",
|
|
46
|
+
"sdk:dev": "concurrently \"pnpm build:watch\" \"pnpm dlx serve -l 3001\""
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import config from '@/config';
|
|
2
|
+
import OpenGameSDK from '@/sdk';
|
|
3
|
+
import { CarouselActions, CarouselMessages, SDKEvents, User } from '@/types';
|
|
4
|
+
import { WidgetBridge } from '@/widget/bridge';
|
|
5
|
+
import { handleWidgetMessage } from '@/widget/messages';
|
|
6
|
+
|
|
7
|
+
let lastProcessedAccessToken: string | null = null;
|
|
8
|
+
let lastProcessedPlayerId: string | null = null;
|
|
9
|
+
let isProcessingCarouselMessage = false;
|
|
10
|
+
|
|
11
|
+
export const handleCarouselMessages = async (self: OpenGameSDK, msg: CarouselMessages) => {
|
|
12
|
+
const isValidAction = Object.values(CarouselActions).includes(msg.action as CarouselActions);
|
|
13
|
+
|
|
14
|
+
if (isValidAction) {
|
|
15
|
+
switch (msg.action) {
|
|
16
|
+
case CarouselActions.SetEmbedId:
|
|
17
|
+
// Skip if we already have a playerId and the embedId matches (no change)
|
|
18
|
+
if (self.playerId && self.embedId === msg.data.embedId) {
|
|
19
|
+
console.log(
|
|
20
|
+
`[Carousel] Skipping SetEmbedId - embedId unchanged and playerId already set`,
|
|
21
|
+
);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// If accessToken is provided, check if we've already processed this exact token
|
|
26
|
+
if (msg.data.accessToken) {
|
|
27
|
+
// Skip if this is the same accessToken we already successfully processed
|
|
28
|
+
if (
|
|
29
|
+
lastProcessedAccessToken === msg.data.accessToken &&
|
|
30
|
+
self.playerId &&
|
|
31
|
+
self.hasSession
|
|
32
|
+
) {
|
|
33
|
+
console.log(`[Carousel] Skipping SetEmbedId - accessToken already processed`);
|
|
34
|
+
self.setEmbedId(msg.data.embedId);
|
|
35
|
+
self.setContext(msg.data.context);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Prevent concurrent processing of the same message
|
|
40
|
+
if (isProcessingCarouselMessage) {
|
|
41
|
+
console.log(`[Carousel] Skipping SetEmbedId - already processing a message`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (self.gameId) {
|
|
46
|
+
try {
|
|
47
|
+
isProcessingCarouselMessage = true;
|
|
48
|
+
|
|
49
|
+
const res = await fetch(`/user/me`, {
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${msg.data.accessToken}`,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
throw new Error('Failed to fetch user');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const me = (await res.json()) as User;
|
|
60
|
+
|
|
61
|
+
// Store this accessToken as processed
|
|
62
|
+
lastProcessedAccessToken = msg.data.accessToken;
|
|
63
|
+
lastProcessedPlayerId = me.id;
|
|
64
|
+
|
|
65
|
+
await self.loadGame(self.gameId, me.id);
|
|
66
|
+
} finally {
|
|
67
|
+
isProcessingCarouselMessage = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (msg.data.isDashboard !== undefined) {
|
|
72
|
+
const oldIsDashboard = self.isDashboard;
|
|
73
|
+
self.isDashboard = msg.data.isDashboard;
|
|
74
|
+
if (oldIsDashboard !== msg.data.isDashboard) {
|
|
75
|
+
const newWidgetUrl = self.widgetUrl;
|
|
76
|
+
|
|
77
|
+
const iframes = document.querySelectorAll('iframe');
|
|
78
|
+
iframes.forEach((iframe) => {
|
|
79
|
+
const src = iframe.src || '';
|
|
80
|
+
if (
|
|
81
|
+
src.startsWith(config.widgetURL) ||
|
|
82
|
+
(config.dashboardURL && src.startsWith(config.dashboardURL))
|
|
83
|
+
) {
|
|
84
|
+
iframe.remove();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
self.widget = new WidgetBridge((msg) => handleWidgetMessage(self, msg), newWidgetUrl);
|
|
89
|
+
self.widget.init(document.body, newWidgetUrl).catch((e: unknown) => {
|
|
90
|
+
console.error(
|
|
91
|
+
`Failed to re-initialize widget: ${e instanceof Error ? e.message : e}`,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
self.setEmbedId(msg.data.embedId);
|
|
97
|
+
self.setContext(msg.data.context);
|
|
98
|
+
return;
|
|
99
|
+
case CarouselActions.SetReferral:
|
|
100
|
+
self.setReferrer(msg.data.gameReferrer, msg.data.referredGameId);
|
|
101
|
+
self.setEmbedId(msg.data.embedId);
|
|
102
|
+
return;
|
|
103
|
+
case CarouselActions.SetDistributor:
|
|
104
|
+
self.setDistributorKeys(msg.data.distributorKeys);
|
|
105
|
+
return;
|
|
106
|
+
case CarouselActions.SetPlayerId:
|
|
107
|
+
if (self.embedId !== msg.data.embedId) {
|
|
108
|
+
console.warn(`[Carousel] Embed ID mismatch. Skipping player ID update.`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Skip if this is the same playerId we already have and we have a valid session
|
|
113
|
+
if (self.playerId === msg.data.playerId && self.hasSession) {
|
|
114
|
+
console.log(
|
|
115
|
+
`[Carousel] Skipping SetPlayerId - same playerId and already have valid session`,
|
|
116
|
+
);
|
|
117
|
+
return self.emit(SDKEvents.OnLoginSuccess);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Track the playerId to avoid duplicate processing
|
|
121
|
+
if (lastProcessedPlayerId === msg.data.playerId && self.hasSession) {
|
|
122
|
+
console.log(`[Carousel] Skipping SetPlayerId - playerId already processed`);
|
|
123
|
+
return self.emit(SDKEvents.OnLoginSuccess);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (self.gameId) {
|
|
127
|
+
lastProcessedPlayerId = msg.data.playerId;
|
|
128
|
+
await self.loadGame(self.gameId, msg.data.playerId);
|
|
129
|
+
}
|
|
130
|
+
return self.emit(SDKEvents.OnLoginSuccess);
|
|
131
|
+
case CarouselActions.OnClaimError:
|
|
132
|
+
if (msg.data.reason === 'Signature interrupted by user') {
|
|
133
|
+
return self.emit(SDKEvents.OnClaimCancelled, {
|
|
134
|
+
reason: msg.data.reason,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return self.emit(SDKEvents.OnClaimFailed, {
|
|
138
|
+
reason: msg.data.reason,
|
|
139
|
+
});
|
|
140
|
+
default:
|
|
141
|
+
console.warn(`Invalid action received from carousel: ${msg}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const isDevMode = process.env.NODE_ENV !== 'production';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if a hostname is a development origin (localhost, ngrok, railway)
|
|
5
|
+
* Only returns true in dev mode.
|
|
6
|
+
*/
|
|
7
|
+
export const isDevOrigin = (hostname: string): boolean => {
|
|
8
|
+
if (!isDevMode) return false;
|
|
9
|
+
return (
|
|
10
|
+
hostname === 'localhost' ||
|
|
11
|
+
hostname.includes('localhost') ||
|
|
12
|
+
hostname.endsWith('.railway.app') ||
|
|
13
|
+
hostname.endsWith('.ngrok.io') ||
|
|
14
|
+
hostname.endsWith('.ngrok-free.app') ||
|
|
15
|
+
hostname.endsWith('.ngrok.app') ||
|
|
16
|
+
hostname.endsWith('.ngrok.dev')
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
apiURL: process.env.OGP_API_URL || 'https://api.play.fun',
|
|
22
|
+
dashboardURL: process.env.OGP_DASHBOARD_URL || 'https://play.fun',
|
|
23
|
+
widgetURL: process.env.OGP_WIDGET_URL || 'https://widget.play.fun',
|
|
24
|
+
carouselURL: process.env.OGP_CAROUSEL_URL || 'https://carousel.play.fun',
|
|
25
|
+
isDevMode,
|
|
26
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import config from '@/config';
|
|
2
|
+
|
|
3
|
+
interface ApiClientOpts {
|
|
4
|
+
baseUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ApiClient {
|
|
8
|
+
private baseUrl: string = config.apiURL;
|
|
9
|
+
|
|
10
|
+
constructor(opts: ApiClientOpts) {
|
|
11
|
+
this.baseUrl = opts.baseUrl || this.baseUrl;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async fetch<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
|
15
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`, options);
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
const errorText = await response.text();
|
|
18
|
+
const error = new Error(
|
|
19
|
+
errorText || 'API Reuqest failed with status ' + response.status,
|
|
20
|
+
) as Error & { status?: number; response?: { status: number } };
|
|
21
|
+
error.status = response.status;
|
|
22
|
+
error.response = { status: response.status };
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const data = (await response.json()) as T;
|
|
27
|
+
return data as T;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async paginatedFetch<T>(
|
|
31
|
+
endpoint: string,
|
|
32
|
+
options?: RequestInit,
|
|
33
|
+
): Promise<{
|
|
34
|
+
items: T[];
|
|
35
|
+
nextCursor?: string;
|
|
36
|
+
hasMore: boolean;
|
|
37
|
+
total: number;
|
|
38
|
+
}> {
|
|
39
|
+
const response = await fetch(`${this.baseUrl}${endpoint}`, options);
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const errorText = await response.text();
|
|
42
|
+
const error = new Error(
|
|
43
|
+
errorText || 'API Reuqest failed with status ' + response.status,
|
|
44
|
+
) as Error & { status?: number; response?: { status: number } };
|
|
45
|
+
error.status = response.status;
|
|
46
|
+
error.response = { status: response.status };
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
return data as {
|
|
52
|
+
items: T[];
|
|
53
|
+
nextCursor?: string;
|
|
54
|
+
hasMore: boolean;
|
|
55
|
+
total: number;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
ADDED