@idosgames/mcp 0.1.21 → 0.1.22
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/package.json +1 -1
- package/registry/host.json +1 -1
- package/registry/index.json +99 -99
- package/registry/modules/ai.json +3 -3
- package/registry/modules/board-game.json +4 -4
- package/registry/modules/boosts.json +3 -3
- package/registry/modules/character.json +3 -3
- package/registry/modules/chat.json +3 -3
- package/registry/modules/collections.json +3 -3
- package/registry/modules/coop-events.json +3 -3
- package/registry/modules/craft.json +3 -3
- package/registry/modules/deals.json +3 -3
- package/registry/modules/events.json +3 -3
- package/registry/modules/friends.json +3 -3
- package/registry/modules/idle-rpg.json +4 -4
- package/registry/modules/inventory.json +3 -3
- package/registry/modules/invite.json +3 -3
- package/registry/modules/leaderboard.json +3 -3
- package/registry/modules/lootboxes.json +3 -3
- package/registry/modules/mailbox.json +3 -3
- package/registry/modules/marketplace.json +3 -3
- package/registry/modules/mini-game.json +3 -3
- package/registry/modules/multiplayer.json +3 -3
- package/registry/modules/premium.json +3 -3
- package/registry/modules/profile.json +7 -7
- package/registry/modules/pvp.json +3 -3
- package/registry/modules/quests.json +3 -3
- package/registry/modules/rewards.json +3 -3
- package/registry/modules/season.json +3 -3
- package/registry/modules/store.json +3 -3
- package/registry/modules/tutorials.json +3 -3
- package/registry/modules/voxelcraft.json +2 -2
- package/registry/modules/workshop.json +3 -3
- package/registry/skills/authentication.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idosgames/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"description": "MCP server that serves the iDosGames Module & Skills Registry to AI coding agents (Claude Code, Codex, Cursor…): list/pull composable game modules and the host scaffold, and load skills for @idosgames/core, the module contract, and composition.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/registry/host.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "package.json",
|
|
26
|
-
"content": "{\n \"name\": \"@idosgames/base-template\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"The base every AI Coder project starts from: sign-in and the lobby (src/base/) on the host shell. The systems (shop, heroes, leaderboards…) and the games are modules plugged in via src/modules.ts.\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\"\n },\n \"//\": \"Versions are pinned exactly: this is a seed for AI Coder projects, which build offline against a dependency allowlist baked at these versions (see scripts/pack-builder.mjs). Modules bring their own engine deps (three/phaser) when added.\",\n \"dependencies\": {\n \"@idosgames/app-shell\": \"0.
|
|
26
|
+
"content": "{\n \"name\": \"@idosgames/base-template\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"The base every AI Coder project starts from: sign-in and the lobby (src/base/) on the host shell. The systems (shop, heroes, leaderboards…) and the games are modules plugged in via src/modules.ts.\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\"\n },\n \"//\": \"Versions are pinned exactly: this is a seed for AI Coder projects, which build offline against a dependency allowlist baked at these versions (see scripts/pack-builder.mjs). Modules bring their own engine deps (three/phaser) when added.\",\n \"dependencies\": {\n \"@idosgames/app-shell\": \"0.10.0\",\n \"@idosgames/core\": \"0.20.0\",\n \"@idosgames/module-sdk\": \"0.8.1\",\n \"@idosgames/react\": \"0.4.2\",\n \"@idosgames/wallet\": \"0.6.1\",\n \"@reown/appkit\": \"1.8.23\",\n \"@reown/appkit-adapter-wagmi\": \"1.8.23\",\n \"@tanstack/react-query\": \"5.101.2\",\n \"react\": \"19.2.7\",\n \"react-dom\": \"19.2.7\",\n \"wagmi\": \"3.7.2\"\n },\n \"devDependencies\": {\n \"@types/react\": \"19.2.17\",\n \"@types/react-dom\": \"19.2.3\",\n \"@vitejs/plugin-react\": \"6.0.4\",\n \"typescript\": \"5.9.3\",\n \"vite\": \"8.1.5\"\n }\n}\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "public/sw.js",
|
package/registry/index.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedFromCommit": "
|
|
2
|
+
"generatedFromCommit": "0c700357e0592fa890927b721548cf177f6870ef",
|
|
3
3
|
"runtimePackages": {
|
|
4
|
-
"@idosgames/core": "0.
|
|
5
|
-
"@idosgames/wallet": "0.6.
|
|
6
|
-
"@idosgames/module-sdk": "0.8.
|
|
7
|
-
"@idosgames/react": "0.4.
|
|
8
|
-
"@idosgames/app-shell": "0.
|
|
4
|
+
"@idosgames/core": "0.20.0",
|
|
5
|
+
"@idosgames/wallet": "0.6.1",
|
|
6
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
7
|
+
"@idosgames/react": "0.4.2",
|
|
8
|
+
"@idosgames/app-shell": "0.10.0"
|
|
9
9
|
},
|
|
10
10
|
"host": {
|
|
11
11
|
"id": "base",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
},
|
|
46
46
|
"version": "0.1.1",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@idosgames/core": "0.
|
|
49
|
-
"@idosgames/module-sdk": "0.8.
|
|
50
|
-
"@idosgames/react": "0.4.
|
|
48
|
+
"@idosgames/core": "0.20.0",
|
|
49
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
50
|
+
"@idosgames/react": "0.4.2",
|
|
51
51
|
"react": "19.2.7"
|
|
52
52
|
},
|
|
53
53
|
"fileCount": 6,
|
|
@@ -82,9 +82,9 @@
|
|
|
82
82
|
},
|
|
83
83
|
"version": "0.1.1",
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@idosgames/core": "0.
|
|
86
|
-
"@idosgames/module-sdk": "0.8.
|
|
87
|
-
"@idosgames/react": "0.4.
|
|
85
|
+
"@idosgames/core": "0.20.0",
|
|
86
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
87
|
+
"@idosgames/react": "0.4.2",
|
|
88
88
|
"react": "19.2.7"
|
|
89
89
|
},
|
|
90
90
|
"fileCount": 6,
|
|
@@ -126,9 +126,9 @@
|
|
|
126
126
|
},
|
|
127
127
|
"version": "0.1.1",
|
|
128
128
|
"dependencies": {
|
|
129
|
-
"@idosgames/core": "0.
|
|
130
|
-
"@idosgames/module-sdk": "0.8.
|
|
131
|
-
"@idosgames/react": "0.4.
|
|
129
|
+
"@idosgames/core": "0.20.0",
|
|
130
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
131
|
+
"@idosgames/react": "0.4.2",
|
|
132
132
|
"react": "19.2.7"
|
|
133
133
|
},
|
|
134
134
|
"fileCount": 6,
|
|
@@ -163,9 +163,9 @@
|
|
|
163
163
|
},
|
|
164
164
|
"version": "0.1.0",
|
|
165
165
|
"dependencies": {
|
|
166
|
-
"@idosgames/core": "0.
|
|
167
|
-
"@idosgames/module-sdk": "0.8.
|
|
168
|
-
"@idosgames/react": "0.4.
|
|
166
|
+
"@idosgames/core": "0.20.0",
|
|
167
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
168
|
+
"@idosgames/react": "0.4.2",
|
|
169
169
|
"react": "19.2.7"
|
|
170
170
|
},
|
|
171
171
|
"fileCount": 6,
|
|
@@ -200,9 +200,9 @@
|
|
|
200
200
|
},
|
|
201
201
|
"version": "0.1.0",
|
|
202
202
|
"dependencies": {
|
|
203
|
-
"@idosgames/core": "0.
|
|
204
|
-
"@idosgames/module-sdk": "0.8.
|
|
205
|
-
"@idosgames/react": "0.4.
|
|
203
|
+
"@idosgames/core": "0.20.0",
|
|
204
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
205
|
+
"@idosgames/react": "0.4.2",
|
|
206
206
|
"react": "19.2.7"
|
|
207
207
|
},
|
|
208
208
|
"fileCount": 6,
|
|
@@ -236,9 +236,9 @@
|
|
|
236
236
|
},
|
|
237
237
|
"version": "0.1.0",
|
|
238
238
|
"dependencies": {
|
|
239
|
-
"@idosgames/core": "0.
|
|
240
|
-
"@idosgames/module-sdk": "0.8.
|
|
241
|
-
"@idosgames/react": "0.4.
|
|
239
|
+
"@idosgames/core": "0.20.0",
|
|
240
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
241
|
+
"@idosgames/react": "0.4.2",
|
|
242
242
|
"react": "19.2.7"
|
|
243
243
|
},
|
|
244
244
|
"fileCount": 6,
|
|
@@ -277,9 +277,9 @@
|
|
|
277
277
|
},
|
|
278
278
|
"version": "0.1.1",
|
|
279
279
|
"dependencies": {
|
|
280
|
-
"@idosgames/core": "0.
|
|
281
|
-
"@idosgames/module-sdk": "0.8.
|
|
282
|
-
"@idosgames/react": "0.4.
|
|
280
|
+
"@idosgames/core": "0.20.0",
|
|
281
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
282
|
+
"@idosgames/react": "0.4.2",
|
|
283
283
|
"react": "19.2.7"
|
|
284
284
|
},
|
|
285
285
|
"fileCount": 6,
|
|
@@ -313,9 +313,9 @@
|
|
|
313
313
|
},
|
|
314
314
|
"version": "0.1.1",
|
|
315
315
|
"dependencies": {
|
|
316
|
-
"@idosgames/core": "0.
|
|
317
|
-
"@idosgames/module-sdk": "0.8.
|
|
318
|
-
"@idosgames/react": "0.4.
|
|
316
|
+
"@idosgames/core": "0.20.0",
|
|
317
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
318
|
+
"@idosgames/react": "0.4.2",
|
|
319
319
|
"react": "19.2.7"
|
|
320
320
|
},
|
|
321
321
|
"fileCount": 6,
|
|
@@ -352,9 +352,9 @@
|
|
|
352
352
|
},
|
|
353
353
|
"version": "0.1.0",
|
|
354
354
|
"dependencies": {
|
|
355
|
-
"@idosgames/core": "0.
|
|
356
|
-
"@idosgames/module-sdk": "0.8.
|
|
357
|
-
"@idosgames/react": "0.4.
|
|
355
|
+
"@idosgames/core": "0.20.0",
|
|
356
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
357
|
+
"@idosgames/react": "0.4.2",
|
|
358
358
|
"react": "19.2.7"
|
|
359
359
|
},
|
|
360
360
|
"fileCount": 6,
|
|
@@ -391,9 +391,9 @@
|
|
|
391
391
|
},
|
|
392
392
|
"version": "0.1.1",
|
|
393
393
|
"dependencies": {
|
|
394
|
-
"@idosgames/core": "0.
|
|
395
|
-
"@idosgames/module-sdk": "0.8.
|
|
396
|
-
"@idosgames/react": "0.4.
|
|
394
|
+
"@idosgames/core": "0.20.0",
|
|
395
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
396
|
+
"@idosgames/react": "0.4.2",
|
|
397
397
|
"react": "19.2.7"
|
|
398
398
|
},
|
|
399
399
|
"fileCount": 6,
|
|
@@ -431,9 +431,9 @@
|
|
|
431
431
|
},
|
|
432
432
|
"version": "0.1.1",
|
|
433
433
|
"dependencies": {
|
|
434
|
-
"@idosgames/core": "0.
|
|
435
|
-
"@idosgames/module-sdk": "0.8.
|
|
436
|
-
"@idosgames/react": "0.4.
|
|
434
|
+
"@idosgames/core": "0.20.0",
|
|
435
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
436
|
+
"@idosgames/react": "0.4.2",
|
|
437
437
|
"react": "19.2.7"
|
|
438
438
|
},
|
|
439
439
|
"fileCount": 6,
|
|
@@ -472,9 +472,9 @@
|
|
|
472
472
|
},
|
|
473
473
|
"version": "0.1.1",
|
|
474
474
|
"dependencies": {
|
|
475
|
-
"@idosgames/core": "0.
|
|
476
|
-
"@idosgames/module-sdk": "0.8.
|
|
477
|
-
"@idosgames/react": "0.4.
|
|
475
|
+
"@idosgames/core": "0.20.0",
|
|
476
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
477
|
+
"@idosgames/react": "0.4.2",
|
|
478
478
|
"react": "19.2.7"
|
|
479
479
|
},
|
|
480
480
|
"fileCount": 6,
|
|
@@ -511,9 +511,9 @@
|
|
|
511
511
|
},
|
|
512
512
|
"version": "0.1.1",
|
|
513
513
|
"dependencies": {
|
|
514
|
-
"@idosgames/core": "0.
|
|
515
|
-
"@idosgames/module-sdk": "0.8.
|
|
516
|
-
"@idosgames/react": "0.4.
|
|
514
|
+
"@idosgames/core": "0.20.0",
|
|
515
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
516
|
+
"@idosgames/react": "0.4.2",
|
|
517
517
|
"react": "19.2.7"
|
|
518
518
|
},
|
|
519
519
|
"fileCount": 6,
|
|
@@ -553,9 +553,9 @@
|
|
|
553
553
|
},
|
|
554
554
|
"version": "0.1.1",
|
|
555
555
|
"dependencies": {
|
|
556
|
-
"@idosgames/core": "0.
|
|
557
|
-
"@idosgames/module-sdk": "0.8.
|
|
558
|
-
"@idosgames/react": "0.4.
|
|
556
|
+
"@idosgames/core": "0.20.0",
|
|
557
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
558
|
+
"@idosgames/react": "0.4.2",
|
|
559
559
|
"react": "19.2.7"
|
|
560
560
|
},
|
|
561
561
|
"fileCount": 6,
|
|
@@ -590,9 +590,9 @@
|
|
|
590
590
|
},
|
|
591
591
|
"version": "0.1.1",
|
|
592
592
|
"dependencies": {
|
|
593
|
-
"@idosgames/core": "0.
|
|
594
|
-
"@idosgames/module-sdk": "0.8.
|
|
595
|
-
"@idosgames/react": "0.4.
|
|
593
|
+
"@idosgames/core": "0.20.0",
|
|
594
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
595
|
+
"@idosgames/react": "0.4.2",
|
|
596
596
|
"react": "19.2.7"
|
|
597
597
|
},
|
|
598
598
|
"fileCount": 6,
|
|
@@ -631,9 +631,9 @@
|
|
|
631
631
|
},
|
|
632
632
|
"version": "0.1.1",
|
|
633
633
|
"dependencies": {
|
|
634
|
-
"@idosgames/core": "0.
|
|
635
|
-
"@idosgames/module-sdk": "0.8.
|
|
636
|
-
"@idosgames/react": "0.4.
|
|
634
|
+
"@idosgames/core": "0.20.0",
|
|
635
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
636
|
+
"@idosgames/react": "0.4.2",
|
|
637
637
|
"react": "19.2.7"
|
|
638
638
|
},
|
|
639
639
|
"fileCount": 6,
|
|
@@ -671,9 +671,9 @@
|
|
|
671
671
|
},
|
|
672
672
|
"version": "0.1.1",
|
|
673
673
|
"dependencies": {
|
|
674
|
-
"@idosgames/core": "0.
|
|
675
|
-
"@idosgames/module-sdk": "0.8.
|
|
676
|
-
"@idosgames/react": "0.4.
|
|
674
|
+
"@idosgames/core": "0.20.0",
|
|
675
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
676
|
+
"@idosgames/react": "0.4.2",
|
|
677
677
|
"react": "19.2.7"
|
|
678
678
|
},
|
|
679
679
|
"fileCount": 6,
|
|
@@ -711,9 +711,9 @@
|
|
|
711
711
|
},
|
|
712
712
|
"version": "0.1.1",
|
|
713
713
|
"dependencies": {
|
|
714
|
-
"@idosgames/core": "0.
|
|
715
|
-
"@idosgames/module-sdk": "0.8.
|
|
716
|
-
"@idosgames/react": "0.4.
|
|
714
|
+
"@idosgames/core": "0.20.0",
|
|
715
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
716
|
+
"@idosgames/react": "0.4.2",
|
|
717
717
|
"react": "19.2.7"
|
|
718
718
|
},
|
|
719
719
|
"fileCount": 6,
|
|
@@ -751,9 +751,9 @@
|
|
|
751
751
|
},
|
|
752
752
|
"version": "0.1.1",
|
|
753
753
|
"dependencies": {
|
|
754
|
-
"@idosgames/core": "0.
|
|
755
|
-
"@idosgames/module-sdk": "0.8.
|
|
756
|
-
"@idosgames/react": "0.4.
|
|
754
|
+
"@idosgames/core": "0.20.0",
|
|
755
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
756
|
+
"@idosgames/react": "0.4.2",
|
|
757
757
|
"react": "19.2.7"
|
|
758
758
|
},
|
|
759
759
|
"fileCount": 6,
|
|
@@ -792,9 +792,9 @@
|
|
|
792
792
|
},
|
|
793
793
|
"version": "0.1.0",
|
|
794
794
|
"dependencies": {
|
|
795
|
-
"@idosgames/core": "0.
|
|
796
|
-
"@idosgames/module-sdk": "0.8.
|
|
797
|
-
"@idosgames/react": "0.4.
|
|
795
|
+
"@idosgames/core": "0.20.0",
|
|
796
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
797
|
+
"@idosgames/react": "0.4.2",
|
|
798
798
|
"react": "19.2.7"
|
|
799
799
|
},
|
|
800
800
|
"fileCount": 6,
|
|
@@ -830,9 +830,9 @@
|
|
|
830
830
|
},
|
|
831
831
|
"version": "0.1.0",
|
|
832
832
|
"dependencies": {
|
|
833
|
-
"@idosgames/core": "0.
|
|
834
|
-
"@idosgames/module-sdk": "0.8.
|
|
835
|
-
"@idosgames/react": "0.4.
|
|
833
|
+
"@idosgames/core": "0.20.0",
|
|
834
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
835
|
+
"@idosgames/react": "0.4.2",
|
|
836
836
|
"react": "19.2.7"
|
|
837
837
|
},
|
|
838
838
|
"fileCount": 6,
|
|
@@ -870,9 +870,9 @@
|
|
|
870
870
|
},
|
|
871
871
|
"version": "0.1.1",
|
|
872
872
|
"dependencies": {
|
|
873
|
-
"@idosgames/core": "0.
|
|
874
|
-
"@idosgames/module-sdk": "0.8.
|
|
875
|
-
"@idosgames/react": "0.4.
|
|
873
|
+
"@idosgames/core": "0.20.0",
|
|
874
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
875
|
+
"@idosgames/react": "0.4.2",
|
|
876
876
|
"react": "19.2.7"
|
|
877
877
|
},
|
|
878
878
|
"fileCount": 7,
|
|
@@ -908,15 +908,15 @@
|
|
|
908
908
|
"name": "iDos Games",
|
|
909
909
|
"url": "https://idosgames.com"
|
|
910
910
|
},
|
|
911
|
-
"version": "0.1.
|
|
911
|
+
"version": "0.1.2",
|
|
912
912
|
"dependencies": {
|
|
913
|
-
"@idosgames/core": "0.
|
|
914
|
-
"@idosgames/module-sdk": "0.8.
|
|
915
|
-
"@idosgames/react": "0.4.
|
|
913
|
+
"@idosgames/core": "0.20.0",
|
|
914
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
915
|
+
"@idosgames/react": "0.4.2",
|
|
916
916
|
"react": "19.2.7"
|
|
917
917
|
},
|
|
918
918
|
"fileCount": 6,
|
|
919
|
-
"contentHash": "
|
|
919
|
+
"contentHash": "bcb4c2b1d773155f758fb2cde1ae7cf4a70f98b7bc50b37bfba2e9b83fc0d101"
|
|
920
920
|
},
|
|
921
921
|
{
|
|
922
922
|
"id": "tutorials",
|
|
@@ -947,9 +947,9 @@
|
|
|
947
947
|
},
|
|
948
948
|
"version": "0.1.1",
|
|
949
949
|
"dependencies": {
|
|
950
|
-
"@idosgames/core": "0.
|
|
951
|
-
"@idosgames/module-sdk": "0.8.
|
|
952
|
-
"@idosgames/react": "0.4.
|
|
950
|
+
"@idosgames/core": "0.20.0",
|
|
951
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
952
|
+
"@idosgames/react": "0.4.2",
|
|
953
953
|
"react": "19.2.7"
|
|
954
954
|
},
|
|
955
955
|
"fileCount": 6,
|
|
@@ -991,9 +991,9 @@
|
|
|
991
991
|
},
|
|
992
992
|
"version": "0.1.2",
|
|
993
993
|
"dependencies": {
|
|
994
|
-
"@idosgames/core": "0.
|
|
995
|
-
"@idosgames/module-sdk": "0.8.
|
|
996
|
-
"@idosgames/react": "0.4.
|
|
994
|
+
"@idosgames/core": "0.20.0",
|
|
995
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
996
|
+
"@idosgames/react": "0.4.2",
|
|
997
997
|
"react": "19.2.7"
|
|
998
998
|
},
|
|
999
999
|
"fileCount": 5,
|
|
@@ -1026,9 +1026,9 @@
|
|
|
1026
1026
|
},
|
|
1027
1027
|
"version": "0.1.1",
|
|
1028
1028
|
"dependencies": {
|
|
1029
|
-
"@idosgames/core": "0.
|
|
1030
|
-
"@idosgames/module-sdk": "0.8.
|
|
1031
|
-
"@idosgames/react": "0.4.
|
|
1029
|
+
"@idosgames/core": "0.20.0",
|
|
1030
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
1031
|
+
"@idosgames/react": "0.4.2",
|
|
1032
1032
|
"react": "19.2.7"
|
|
1033
1033
|
},
|
|
1034
1034
|
"fileCount": 6,
|
|
@@ -1076,10 +1076,10 @@
|
|
|
1076
1076
|
]
|
|
1077
1077
|
},
|
|
1078
1078
|
"dependencies": {
|
|
1079
|
-
"@idosgames/core": "0.
|
|
1080
|
-
"@idosgames/module-sdk": "0.8.
|
|
1081
|
-
"@idosgames/react": "0.4.
|
|
1082
|
-
"@idosgames/wallet": "0.6.
|
|
1079
|
+
"@idosgames/core": "0.20.0",
|
|
1080
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
1081
|
+
"@idosgames/react": "0.4.2",
|
|
1082
|
+
"@idosgames/wallet": "0.6.1",
|
|
1083
1083
|
"@tanstack/react-query": "5.101.2",
|
|
1084
1084
|
"react": "19.2.7",
|
|
1085
1085
|
"three": "0.185.1",
|
|
@@ -1135,10 +1135,10 @@
|
|
|
1135
1135
|
]
|
|
1136
1136
|
},
|
|
1137
1137
|
"dependencies": {
|
|
1138
|
-
"@idosgames/core": "0.
|
|
1139
|
-
"@idosgames/module-sdk": "0.8.
|
|
1140
|
-
"@idosgames/react": "0.4.
|
|
1141
|
-
"@idosgames/wallet": "0.6.
|
|
1138
|
+
"@idosgames/core": "0.20.0",
|
|
1139
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
1140
|
+
"@idosgames/react": "0.4.2",
|
|
1141
|
+
"@idosgames/wallet": "0.6.1",
|
|
1142
1142
|
"@tanstack/react-query": "5.101.2",
|
|
1143
1143
|
"phaser": "4.2.1",
|
|
1144
1144
|
"react": "19.2.7",
|
|
@@ -1214,8 +1214,8 @@
|
|
|
1214
1214
|
},
|
|
1215
1215
|
"version": "0.4.1",
|
|
1216
1216
|
"dependencies": {
|
|
1217
|
-
"@idosgames/core": "0.
|
|
1218
|
-
"@idosgames/module-sdk": "0.8.
|
|
1217
|
+
"@idosgames/core": "0.20.0",
|
|
1218
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
1219
1219
|
"three": "0.185.1"
|
|
1220
1220
|
},
|
|
1221
1221
|
"fileCount": 426,
|
|
@@ -1248,9 +1248,9 @@
|
|
|
1248
1248
|
},
|
|
1249
1249
|
"version": "0.1.1",
|
|
1250
1250
|
"dependencies": {
|
|
1251
|
-
"@idosgames/core": "0.
|
|
1252
|
-
"@idosgames/module-sdk": "0.8.
|
|
1253
|
-
"@idosgames/react": "0.4.
|
|
1251
|
+
"@idosgames/core": "0.20.0",
|
|
1252
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
1253
|
+
"@idosgames/react": "0.4.2",
|
|
1254
1254
|
"react": "19.2.7"
|
|
1255
1255
|
},
|
|
1256
1256
|
"fileCount": 7,
|
package/registry/modules/ai.json
CHANGED
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"version": "0.1.1"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@idosgames/core": "0.
|
|
34
|
-
"@idosgames/module-sdk": "0.8.
|
|
35
|
-
"@idosgames/react": "0.4.
|
|
33
|
+
"@idosgames/core": "0.20.0",
|
|
34
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
35
|
+
"@idosgames/react": "0.4.2",
|
|
36
36
|
"react": "19.2.7"
|
|
37
37
|
},
|
|
38
38
|
"contentHash": "09df6751a0e4009a1b8675178a88f9ae49bc33edf36e352f22dbf01dd4482d1e",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@idosgames/core": "0.
|
|
49
|
-
"@idosgames/module-sdk": "0.8.
|
|
50
|
-
"@idosgames/react": "0.4.
|
|
51
|
-
"@idosgames/wallet": "0.6.
|
|
48
|
+
"@idosgames/core": "0.20.0",
|
|
49
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
50
|
+
"@idosgames/react": "0.4.2",
|
|
51
|
+
"@idosgames/wallet": "0.6.1",
|
|
52
52
|
"@tanstack/react-query": "5.101.2",
|
|
53
53
|
"react": "19.2.7",
|
|
54
54
|
"three": "0.185.1",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"version": "0.1.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@idosgames/core": "0.
|
|
35
|
-
"@idosgames/module-sdk": "0.8.
|
|
36
|
-
"@idosgames/react": "0.4.
|
|
34
|
+
"@idosgames/core": "0.20.0",
|
|
35
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
36
|
+
"@idosgames/react": "0.4.2",
|
|
37
37
|
"react": "19.2.7"
|
|
38
38
|
},
|
|
39
39
|
"contentHash": "1c1a852a29a89982e2ed997b1a2356bc4722d00eb4db834ab7472f66f9a8eee3",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"version": "0.1.1"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@idosgames/core": "0.
|
|
43
|
-
"@idosgames/module-sdk": "0.8.
|
|
44
|
-
"@idosgames/react": "0.4.
|
|
42
|
+
"@idosgames/core": "0.20.0",
|
|
43
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
44
|
+
"@idosgames/react": "0.4.2",
|
|
45
45
|
"react": "19.2.7"
|
|
46
46
|
},
|
|
47
47
|
"contentHash": "6f756470b453ff891d612204dbe04e6826000abe0f7a7ba67df240516eb707bb",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"version": "0.1.1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@idosgames/core": "0.
|
|
39
|
-
"@idosgames/module-sdk": "0.8.
|
|
40
|
-
"@idosgames/react": "0.4.
|
|
38
|
+
"@idosgames/core": "0.20.0",
|
|
39
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
40
|
+
"@idosgames/react": "0.4.2",
|
|
41
41
|
"react": "19.2.7"
|
|
42
42
|
},
|
|
43
43
|
"contentHash": "1bd9bec3b0ee8548e800e51bf045b0215da4320231fbf8565350554558f846e4",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"version": "0.1.1"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@idosgames/core": "0.
|
|
38
|
-
"@idosgames/module-sdk": "0.8.
|
|
39
|
-
"@idosgames/react": "0.4.
|
|
37
|
+
"@idosgames/core": "0.20.0",
|
|
38
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
39
|
+
"@idosgames/react": "0.4.2",
|
|
40
40
|
"react": "19.2.7"
|
|
41
41
|
},
|
|
42
42
|
"contentHash": "35feb5ba1e12924999427f0d56b56a5b9db239ee8f91d805bcf0c39b40349273",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"version": "0.1.1"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@idosgames/core": "0.
|
|
36
|
-
"@idosgames/module-sdk": "0.8.
|
|
37
|
-
"@idosgames/react": "0.4.
|
|
35
|
+
"@idosgames/core": "0.20.0",
|
|
36
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
37
|
+
"@idosgames/react": "0.4.2",
|
|
38
38
|
"react": "19.2.7"
|
|
39
39
|
},
|
|
40
40
|
"contentHash": "1637a37f2dc83c83692cd674b2b4c4b5ea959d64ab783bffdb09254e50975836",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"version": "0.1.1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@idosgames/core": "0.
|
|
39
|
-
"@idosgames/module-sdk": "0.8.
|
|
40
|
-
"@idosgames/react": "0.4.
|
|
38
|
+
"@idosgames/core": "0.20.0",
|
|
39
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
40
|
+
"@idosgames/react": "0.4.2",
|
|
41
41
|
"react": "19.2.7"
|
|
42
42
|
},
|
|
43
43
|
"contentHash": "e91d01d089fbe0897748e06bb079be807ad32b921f44c2999b592001c3bd5464",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"version": "0.1.1"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@idosgames/core": "0.
|
|
38
|
-
"@idosgames/module-sdk": "0.8.
|
|
39
|
-
"@idosgames/react": "0.4.
|
|
37
|
+
"@idosgames/core": "0.20.0",
|
|
38
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
39
|
+
"@idosgames/react": "0.4.2",
|
|
40
40
|
"react": "19.2.7"
|
|
41
41
|
},
|
|
42
42
|
"contentHash": "105861fcd680e4cd96311f9cc6a3967278e08f1e0512f43be1f439a13a3503a9",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"version": "0.1.1"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@idosgames/core": "0.
|
|
41
|
-
"@idosgames/module-sdk": "0.8.
|
|
42
|
-
"@idosgames/react": "0.4.
|
|
40
|
+
"@idosgames/core": "0.20.0",
|
|
41
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
42
|
+
"@idosgames/react": "0.4.2",
|
|
43
43
|
"react": "19.2.7"
|
|
44
44
|
},
|
|
45
45
|
"contentHash": "b0f644b550581b65f3d48122c732575e8e4e01bb0e1cfe4fcd22830704dae5e2",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"version": "0.1.1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@idosgames/core": "0.
|
|
39
|
-
"@idosgames/module-sdk": "0.8.
|
|
40
|
-
"@idosgames/react": "0.4.
|
|
38
|
+
"@idosgames/core": "0.20.0",
|
|
39
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
40
|
+
"@idosgames/react": "0.4.2",
|
|
41
41
|
"react": "19.2.7"
|
|
42
42
|
},
|
|
43
43
|
"contentHash": "f5c0187a70b1b22166f4ff70aa228339d4ef0a35acd2837bc3a1129e52576910",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@idosgames/core": "0.
|
|
53
|
-
"@idosgames/module-sdk": "0.8.
|
|
54
|
-
"@idosgames/react": "0.4.
|
|
55
|
-
"@idosgames/wallet": "0.6.
|
|
52
|
+
"@idosgames/core": "0.20.0",
|
|
53
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
54
|
+
"@idosgames/react": "0.4.2",
|
|
55
|
+
"@idosgames/wallet": "0.6.1",
|
|
56
56
|
"@tanstack/react-query": "5.101.2",
|
|
57
57
|
"phaser": "4.2.1",
|
|
58
58
|
"react": "19.2.7",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"version": "0.1.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@idosgames/core": "0.
|
|
36
|
-
"@idosgames/module-sdk": "0.8.
|
|
37
|
-
"@idosgames/react": "0.4.
|
|
35
|
+
"@idosgames/core": "0.20.0",
|
|
36
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
37
|
+
"@idosgames/react": "0.4.2",
|
|
38
38
|
"react": "19.2.7"
|
|
39
39
|
},
|
|
40
40
|
"contentHash": "da42d93b3d21d21109e0f35338c349042dbb01ef442701439dc1d9d4ddbff98d",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"version": "0.1.1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@idosgames/core": "0.
|
|
39
|
-
"@idosgames/module-sdk": "0.8.
|
|
40
|
-
"@idosgames/react": "0.4.
|
|
38
|
+
"@idosgames/core": "0.20.0",
|
|
39
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
40
|
+
"@idosgames/react": "0.4.2",
|
|
41
41
|
"react": "19.2.7"
|
|
42
42
|
},
|
|
43
43
|
"contentHash": "f3ed6702817b2217cee7bf25accd5dbb138d6657bb7f15edcc4cc8c03a26b01f",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"version": "0.1.1"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.
|
|
40
|
-
"@idosgames/module-sdk": "0.8.
|
|
41
|
-
"@idosgames/react": "0.4.
|
|
39
|
+
"@idosgames/core": "0.20.0",
|
|
40
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
41
|
+
"@idosgames/react": "0.4.2",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
44
|
"contentHash": "6d86641300a3b71c007d01eb4bc7ac345eef9ca99e99dda75a40d252c50c23f3",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"version": "0.1.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@idosgames/core": "0.
|
|
36
|
-
"@idosgames/module-sdk": "0.8.
|
|
37
|
-
"@idosgames/react": "0.4.
|
|
35
|
+
"@idosgames/core": "0.20.0",
|
|
36
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
37
|
+
"@idosgames/react": "0.4.2",
|
|
38
38
|
"react": "19.2.7"
|
|
39
39
|
},
|
|
40
40
|
"contentHash": "64fb8cc7fb26c68c838117f01739d5bafcc8da16d797691c651658f1911f527e",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"version": "0.1.0"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.
|
|
40
|
-
"@idosgames/module-sdk": "0.8.
|
|
41
|
-
"@idosgames/react": "0.4.
|
|
39
|
+
"@idosgames/core": "0.20.0",
|
|
40
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
41
|
+
"@idosgames/react": "0.4.2",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
44
|
"contentHash": "b5f960cc3809c920a30dfe655c0531fea691236f418d59107daaffa8a9bbcb66",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"version": "0.1.1"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.
|
|
40
|
-
"@idosgames/module-sdk": "0.8.
|
|
41
|
-
"@idosgames/react": "0.4.
|
|
39
|
+
"@idosgames/core": "0.20.0",
|
|
40
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
41
|
+
"@idosgames/react": "0.4.2",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
44
|
"contentHash": "9c8b1bfa6b479da0a046ff6b5ef61d1f01bec41efafca6941e0075343a780610",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"version": "0.1.1"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@idosgames/core": "0.
|
|
34
|
-
"@idosgames/module-sdk": "0.8.
|
|
35
|
-
"@idosgames/react": "0.4.
|
|
33
|
+
"@idosgames/core": "0.20.0",
|
|
34
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
35
|
+
"@idosgames/react": "0.4.2",
|
|
36
36
|
"react": "19.2.7"
|
|
37
37
|
},
|
|
38
38
|
"contentHash": "eb39139d93bfb84e5a20d2485b081a264234452b5d90978fdf15b940cf3b7ac0",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"version": "0.1.1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@idosgames/core": "0.
|
|
39
|
-
"@idosgames/module-sdk": "0.8.
|
|
40
|
-
"@idosgames/react": "0.4.
|
|
38
|
+
"@idosgames/core": "0.20.0",
|
|
39
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
40
|
+
"@idosgames/react": "0.4.2",
|
|
41
41
|
"react": "19.2.7"
|
|
42
42
|
},
|
|
43
43
|
"contentHash": "14799b9981c650f938ddccbe51d243f5f6aeaf53105d7bd2536dffe6eabf818b",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"version": "0.1.1"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@idosgames/core": "0.
|
|
36
|
-
"@idosgames/module-sdk": "0.8.
|
|
37
|
-
"@idosgames/react": "0.4.
|
|
35
|
+
"@idosgames/core": "0.20.0",
|
|
36
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
37
|
+
"@idosgames/react": "0.4.2",
|
|
38
38
|
"react": "19.2.7"
|
|
39
39
|
},
|
|
40
40
|
"contentHash": "5f2472fa890edc2500ec4f0703aa320a84862d0768d2ce2b576f0460adcc5eca",
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"name": "iDos Games",
|
|
33
33
|
"url": "https://idosgames.com"
|
|
34
34
|
},
|
|
35
|
-
"version": "0.1.
|
|
35
|
+
"version": "0.1.2"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@idosgames/core": "0.
|
|
39
|
-
"@idosgames/module-sdk": "0.8.
|
|
40
|
-
"@idosgames/react": "0.4.
|
|
38
|
+
"@idosgames/core": "0.20.0",
|
|
39
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
40
|
+
"@idosgames/react": "0.4.2",
|
|
41
41
|
"react": "19.2.7"
|
|
42
42
|
},
|
|
43
|
-
"contentHash": "
|
|
43
|
+
"contentHash": "bcb4c2b1d773155f758fb2cde1ae7cf4a70f98b7bc50b37bfba2e9b83fc0d101",
|
|
44
44
|
"files": [
|
|
45
45
|
{
|
|
46
46
|
"path": "i18n.ts",
|
|
@@ -60,11 +60,11 @@
|
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
"path": "ProfileScreen.tsx",
|
|
63
|
-
"content": "import { useCallback, useEffect, useState, type ReactNode } from \"react\";\nimport type { PlayAccessWallet } from \"@idosgames/core\";\nimport type {\n FeatureRegistry,\n FeatureScreenProps,\n} from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient, useUserState } from \"@idosgames/react\";\nimport {\n Button,\n ConfirmPopup,\n Icon,\n Popup,\n Tabs,\n errorText,\n outlined,\n panel,\n usePlayerPrefs,\n useToast,\n useUiKit,\n v,\n} from \"@idosgames/react/ui\";\nimport {\n deleteUnlocked,\n languageChoices,\n nameProblem,\n shortAddress,\n unlinkRefusal,\n volumePercent,\n} from \"./model\";\nimport { t } from \"./i18n\";\n\n// Profile and settings (Unity Alikhan/Username, UI/HUD/Settings, Runtime/UI/Consent, Localization):\n// the name and ID, sound, volume and motion (kept on this device), the game's language, tutorials,\n// linked wallets, logging out and deleting the account. Renaming, unlinking a wallet and deleting are\n// popups. No analytics switch (owner's decision 29.09.2026): anonymous analytics stays on as the SDK\n// ships it (client.analytics).\n\nexport function makeProfileScreen(features: FeatureRegistry) {\n return function ProfileScreen(_: FeatureScreenProps): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n const toast = useToast();\n const kit = useUiKit();\n const [prefs, setPrefs] = usePlayerPrefs();\n const [name, setName] = useState<string | null>(null);\n const [renaming, setRenaming] = useState(false);\n const [ask, setAsk] = useState<\"logout\" | \"delete\" | null>(null);\n const [locale, setLocale] = useState(client.localization.locale ?? \"\");\n const id = client.auth.context?.userID ?? \"\";\n const shown = name ?? state?.PublicData?.Username ?? t(\"guest\");\n const languages = languageChoices(client.localization.locales);\n\n const switchLanguage = async (next: string) => {\n const res = await client.localization.setLocale(next);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n setLocale(res.data);\n };\n\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 1fr)\",\n gap: 12,\n maxWidth: 560,\n margin: \"0 auto\",\n width: \"100%\",\n }}\n >\n <div\n style={{\n ...panel,\n padding: 16,\n display: \"grid\",\n justifyItems: \"center\",\n gap: 8,\n }}\n >\n <div\n style={{\n width: 84,\n height: 84,\n borderRadius: \"50%\",\n display: \"grid\",\n placeItems: \"center\",\n background: `linear-gradient(180deg, ${v.blue}, ${v.blueDeep})`,\n border: `3px solid color-mix(in srgb, ${v.text} 70%, transparent)`,\n ...outlined,\n fontSize: 36,\n }}\n >\n {(shown.trim()[0] ?? \"?\").toUpperCase()}\n </div>\n <div style={{ ...outlined, fontSize: 20 }}>{shown}</div>\n <Button\n size=\"sm\"\n tone=\"blue\"\n onClick={() => setRenaming(true)}\n data-tutorial-anchor=\"profile:rename\"\n >\n <Icon glyph=\"edit\" size={16} /> {t(\"rename\")}\n </Button>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n maxWidth: \"100%\",\n }}\n >\n <span style={{ ...outlined, fontSize: 11, color: v.textDim }}>\n {t(\"playerID\")}\n </span>\n <span\n style={{\n fontFamily: \"monospace\",\n fontSize: 12,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n }}\n >\n {id}\n </span>\n <button\n type=\"button\"\n aria-label={t(\"playerID\")}\n onClick={() =>\n void navigator.clipboard\n ?.writeText(id)\n .then(() => toast(t(\"copied\")))\n }\n style={{\n background: \"none\",\n border: \"none\",\n cursor: \"pointer\",\n padding: 2,\n }}\n >\n <Icon glyph=\"copy\" size={16} />\n </button>\n </div>\n </div>\n\n <div style={{ ...panel, padding: 12, display: \"grid\", gap: 12 }}>\n <div style={{ ...outlined, fontSize: 14, color: v.textDim }}>\n {t(\"settings\")}\n </div>\n <Toggle\n glyph={prefs.muted ? \"mute\" : \"sound\"}\n label={t(\"sound\")}\n value={!prefs.muted}\n onChange={(on) => {\n setPrefs({ muted: !on });\n if (on) kit.play(\"click\");\n }}\n />\n <div\n style={{ display: \"grid\", gap: 4, opacity: prefs.muted ? 0.5 : 1 }}\n >\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n ...outlined,\n fontSize: 13,\n }}\n >\n <span>{t(\"volume\")}</span>\n <span>{volumePercent(prefs.volume)}%</span>\n </div>\n <input\n type=\"range\"\n min={0}\n max={100}\n step={5}\n disabled={prefs.muted}\n value={volumePercent(prefs.volume)}\n onChange={(e) =>\n setPrefs({ volume: Number(e.target.value) / 100 })\n }\n onPointerUp={() => kit.play(\"coin\")}\n style={{ width: \"100%\", accentColor: v.gold }}\n />\n </div>\n <Toggle\n glyph=\"flame\"\n label={t(\"motion\")}\n value={prefs.reducedMotion}\n onChange={(on) => setPrefs({ reducedMotion: on })}\n />\n {languages.length > 1 ? (\n <div style={{ display: \"grid\", gap: 6 }}>\n <div style={{ ...outlined, fontSize: 13 }}>{t(\"language\")}</div>\n <Tabs\n tabs={languages.map((l) => ({ id: l.id, label: l.name }))}\n value={\n languages.find((l) => locale.startsWith(l.id))?.id ??\n languages[0]!.id\n }\n onChange={(l) => void switchLanguage(l)}\n />\n </div>\n ) : null}\n </div>\n\n <WalletsSection />\n\n {features.get(\"tutorials\") ? (\n <Button tone=\"blue\" onClick={() => features.open(\"tutorials\")}>\n <Icon glyph=\"bulb\" size={18} /> {t(\"tutorials\")}\n </Button>\n ) : null}\n <Button tone=\"grey\" onClick={() => setAsk(\"logout\")}>\n <Icon glyph=\"logout\" size={18} /> {t(\"logout\")}\n </Button>\n <Button tone=\"red\" onClick={() => setAsk(\"delete\")}>\n <Icon glyph=\"trash\" size={18} /> {t(\"delete\")}\n </Button>\n\n {renaming ? (\n <RenamePopup\n current={shown}\n onClose={() => setRenaming(false)}\n onDone={(n) => {\n setName(n);\n setRenaming(false);\n }}\n />\n ) : null}\n {ask === \"logout\" ? (\n <ConfirmPopup\n title={t(\"logout\")}\n tone=\"red\"\n onClose={() => setAsk(null)}\n onConfirm={() => {\n client.auth.logout();\n location.reload();\n }}\n >\n <div style={{ ...outlined, fontSize: 14, textAlign: \"center\" }}>\n {t(\"logoutAsk\")}\n </div>\n </ConfirmPopup>\n ) : null}\n {ask === \"delete\" ? <DeletePopup onClose={() => setAsk(null)} /> : null}\n </div>\n );\n };\n}\n\nfunction Toggle({\n glyph,\n label,\n value,\n onChange,\n}: {\n glyph: \"sound\" | \"mute\" | \"flame\" | \"globe\";\n label: string;\n value: boolean;\n onChange: (on: boolean) => void;\n}): ReactNode {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={value}\n onClick={() => onChange(!value)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n background: \"none\",\n border: \"none\",\n color: v.text,\n cursor: \"pointer\",\n padding: 0,\n textAlign: \"left\",\n }}\n >\n <Icon glyph={glyph} size={24} />\n <span style={{ flex: 1, ...outlined, fontSize: 14 }}>{label}</span>\n <span\n style={{\n width: 52,\n height: 30,\n borderRadius: 15,\n background: value ? v.green : v.wellSoft,\n boxShadow: `inset 0 0 0 1px ${v.line}`,\n position: \"relative\",\n transition: \"background .2s\",\n }}\n >\n <span\n style={{\n position: \"absolute\",\n top: 3,\n left: value ? 25 : 3,\n width: 24,\n height: 24,\n borderRadius: \"50%\",\n background: \"#fff\",\n transition: \"left .2s\",\n boxShadow: \"0 2px 4px rgba(0,0,0,.3)\",\n }}\n />\n </span>\n </button>\n );\n}\n\n/**\n * Wallets linked by signature, each with \"Unlink\" (`client.auth.unlinkWallet`). Hidden while there are\n * none. The wallet an account signs in with has no button: the server refuses it (`CanUnlink`).\n */\nfunction WalletsSection(): ReactNode {\n const client = useIDosGamesClient();\n const toast = useToast();\n const [wallets, setWallets] = useState<PlayAccessWallet[]>([]);\n const [asking, setAsking] = useState<PlayAccessWallet | null>(null);\n\n const load = useCallback(async () => {\n const res = await client.blockchain.getPlayAccess();\n if (res.ok) setWallets(res.data.LinkedWallets ?? []);\n }, [client]);\n useEffect(() => {\n void load();\n }, [load]);\n\n // The popup shows the spinner and closes itself when this resolves.\n const unlink = async (wallet: PlayAccessWallet) => {\n const res = await client.auth.unlinkWallet(wallet.NetworkID ?? \"\");\n if (!res.ok) {\n const known = unlinkRefusal(res.error);\n toast(known ? t(known) : errorText(res.error), \"error\");\n } else toast(t(\"unlinked\"), \"success\");\n await load();\n };\n\n if (wallets.length === 0) return null;\n return (\n <div style={{ ...panel, padding: 12, display: \"grid\", gap: 10 }}>\n <div style={{ ...outlined, fontSize: 14, color: v.textDim }}>\n {t(\"wallets\")}\n </div>\n {wallets.map((w) => (\n <div\n key={`${w.NetworkID}:${w.Address}`}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n minWidth: 0,\n }}\n >\n <Icon glyph=\"link\" size={22} />\n <div style={{ flex: 1, minWidth: 0, display: \"grid\" }}>\n <span\n style={{\n fontFamily: \"monospace\",\n fontSize: 13,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n }}\n >\n {shortAddress(w.Address)}\n </span>\n <span style={{ ...outlined, fontSize: 11, color: v.textDim }}>\n {(w.NetworkID ?? \"\").toUpperCase()}\n {w.CanUnlink === false ? ` · ${t(\"loginWallet\")}` : \"\"}\n </span>\n </div>\n {w.CanUnlink !== false ? (\n <Button size=\"sm\" tone=\"grey\" onClick={() => setAsking(w)}>\n {t(\"unlink\")}\n </Button>\n ) : null}\n </div>\n ))}\n {asking ? (\n <ConfirmPopup\n title={t(\"unlinkTitle\")}\n confirmLabel={t(\"unlink\")}\n tone=\"red\"\n onClose={() => setAsking(null)}\n onConfirm={() => unlink(asking)}\n >\n <div\n style={{\n fontFamily: \"monospace\",\n fontSize: 14,\n textAlign: \"center\",\n }}\n >\n {shortAddress(asking.Address)}\n </div>\n <div\n style={{\n ...outlined,\n fontSize: 13,\n textAlign: \"center\",\n lineHeight: 1.4,\n }}\n >\n {t(\"unlinkAsk\")}\n </div>\n </ConfirmPopup>\n ) : null}\n </div>\n );\n}\n\nfunction RenamePopup({\n current,\n onClose,\n onDone,\n}: {\n current: string;\n onClose: () => void;\n onDone: (name: string) => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const toast = useToast();\n const [value, setValue] = useState(current);\n const [busy, setBusy] = useState(false);\n const problem = nameProblem(value);\n const save = async () => {\n if (problem) return;\n setBusy(true);\n const res = await client.user.changeUsername(value.trim());\n setBusy(false);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n toast(t(\"renamed\"), \"success\");\n // The SDK does not patch the cached name — the screen shows the server's answer.\n onDone(res.data.Username ?? value.trim());\n };\n return (\n <Popup title={t(\"rename\")} onClose={onClose} width={400}>\n <input\n value={value}\n autoFocus\n maxLength={40}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={(e) => e.key === \"Enter\" && void save()}\n placeholder={t(\"newName\")}\n style={{\n ...panel,\n padding: \"12px 14px\",\n fontSize: 16,\n color: v.text,\n outline: \"none\",\n }}\n />\n {problem ? (\n <div style={{ ...outlined, fontSize: 12, color: v.red }}>\n {problem === \"short\" ? t(\"nameShort\") : t(\"nameLong\")}\n </div>\n ) : null}\n <Button\n tone=\"green\"\n disabled={!!problem}\n busy={busy}\n onClick={() => void save()}\n >\n {t(\"save\")}\n </Button>\n </Popup>\n );\n}\n\nfunction DeletePopup({ onClose }: { onClose: () => void }): ReactNode {\n const client = useIDosGamesClient();\n const toast = useToast();\n const [typed, setTyped] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const word = t(\"deleteWord\");\n const remove = async () => {\n setBusy(true);\n const res = await client.user.deleteUserAccount();\n setBusy(false);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n toast(t(\"deleted\"));\n client.auth.logout();\n location.reload();\n };\n return (\n <Popup title={t(\"delete\")} onClose={onClose} width={420}>\n <div style={{ ...outlined, fontSize: 14, textAlign: \"center\" }}>\n {t(\"deleteAsk\")} <span style={{ color: v.red }}>{word}</span>\n </div>\n <input\n value={typed}\n onChange={(e) => setTyped(e.target.value)}\n placeholder={word}\n style={{\n ...panel,\n padding: \"12px 14px\",\n fontSize: 16,\n color: v.text,\n outline: \"none\",\n textAlign: \"center\",\n }}\n />\n <Button\n tone=\"red\"\n disabled={!deleteUnlocked(typed, word)}\n busy={busy}\n onClick={() => void remove()}\n >\n {t(\"delete\")}\n </Button>\n </Popup>\n );\n}\n"
|
|
63
|
+
"content": "import { useCallback, useEffect, useState, type ReactNode } from \"react\";\nimport type { PlayAccessWallet } from \"@idosgames/core\";\nimport type {\n FeatureRegistry,\n FeatureScreenProps,\n} from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient, useUserState } from \"@idosgames/react\";\nimport {\n Button,\n ConfirmPopup,\n Icon,\n Popup,\n Tabs,\n errorText,\n outlined,\n panel,\n usePlayerPrefs,\n useToast,\n useUiKit,\n v,\n} from \"@idosgames/react/ui\";\nimport {\n deleteUnlocked,\n languageChoices,\n nameProblem,\n shortAddress,\n unlinkRefusal,\n volumePercent,\n} from \"./model\";\nimport { t } from \"./i18n\";\n\n// Profile and settings (Unity Alikhan/Username, UI/HUD/Settings, Runtime/UI/Consent, Localization):\n// the name and ID, sound, volume and motion (kept on this device), the game's language, tutorials,\n// linked wallets, logging out and deleting the account. Renaming, unlinking a wallet and deleting are\n// popups. No analytics switch (owner's decision 29.09.2026): anonymous analytics stays on as the SDK\n// ships it (client.analytics).\n\nexport function makeProfileScreen(features: FeatureRegistry) {\n return function ProfileScreen(_: FeatureScreenProps): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n const toast = useToast();\n const kit = useUiKit();\n const [prefs, setPrefs] = usePlayerPrefs();\n const [name, setName] = useState<string | null>(null);\n const [renaming, setRenaming] = useState(false);\n const [ask, setAsk] = useState<\"logout\" | \"delete\" | null>(null);\n const [locale, setLocale] = useState(client.localization.locale ?? \"\");\n const id = client.auth.context?.userID ?? \"\";\n const shown = name ?? state?.PublicData?.Username ?? t(\"guest\");\n const languages = languageChoices(client.localization.locales);\n\n const switchLanguage = async (next: string) => {\n const res = await client.localization.setLocale(next);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n setLocale(res.data);\n };\n\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 1fr)\",\n gap: 12,\n maxWidth: 560,\n margin: \"0 auto\",\n width: \"100%\",\n }}\n >\n <div\n style={{\n ...panel,\n padding: 16,\n display: \"grid\",\n justifyItems: \"center\",\n gap: 8,\n }}\n >\n <div\n style={{\n width: 84,\n height: 84,\n borderRadius: \"50%\",\n display: \"grid\",\n placeItems: \"center\",\n background: `linear-gradient(180deg, ${v.blue}, ${v.blueDeep})`,\n border: `3px solid color-mix(in srgb, ${v.text} 70%, transparent)`,\n ...outlined,\n fontSize: 36,\n }}\n >\n {(shown.trim()[0] ?? \"?\").toUpperCase()}\n </div>\n <div style={{ ...outlined, fontSize: 20 }}>{shown}</div>\n <Button\n size=\"sm\"\n tone=\"blue\"\n onClick={() => setRenaming(true)}\n data-tutorial-anchor=\"profile:rename\"\n >\n <Icon glyph=\"edit\" size={16} /> {t(\"rename\")}\n </Button>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n maxWidth: \"100%\",\n }}\n >\n <span style={{ ...outlined, fontSize: 11, color: v.textDim }}>\n {t(\"playerID\")}\n </span>\n <span\n style={{\n fontFamily: \"monospace\",\n fontSize: 12,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n }}\n >\n {id}\n </span>\n <button\n type=\"button\"\n aria-label={t(\"playerID\")}\n onClick={() =>\n void navigator.clipboard\n ?.writeText(id)\n .then(() => toast(t(\"copied\")))\n }\n style={{\n background: \"none\",\n border: \"none\",\n cursor: \"pointer\",\n padding: 2,\n }}\n >\n <Icon glyph=\"copy\" size={16} />\n </button>\n </div>\n </div>\n\n <div style={{ ...panel, padding: 12, display: \"grid\", gap: 12 }}>\n <div style={{ ...outlined, fontSize: 14, color: v.textDim }}>\n {t(\"settings\")}\n </div>\n <Toggle\n glyph={prefs.muted ? \"mute\" : \"sound\"}\n label={t(\"sound\")}\n value={!prefs.muted}\n onChange={(on) => {\n setPrefs({ muted: !on });\n if (on) kit.play(\"click\");\n }}\n />\n <div\n style={{ display: \"grid\", gap: 4, opacity: prefs.muted ? 0.5 : 1 }}\n >\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n ...outlined,\n fontSize: 13,\n }}\n >\n <span>{t(\"volume\")}</span>\n <span>{volumePercent(prefs.volume)}%</span>\n </div>\n <input\n type=\"range\"\n min={0}\n max={100}\n step={5}\n disabled={prefs.muted}\n value={volumePercent(prefs.volume)}\n onChange={(e) =>\n setPrefs({ volume: Number(e.target.value) / 100 })\n }\n onPointerUp={() => kit.play(\"coin\")}\n style={{ width: \"100%\", accentColor: v.gold }}\n />\n </div>\n <Toggle\n glyph=\"flame\"\n label={t(\"motion\")}\n value={prefs.reducedMotion}\n onChange={(on) => setPrefs({ reducedMotion: on })}\n />\n {languages.length > 1 ? (\n <div style={{ display: \"grid\", gap: 6 }}>\n <div style={{ ...outlined, fontSize: 13 }}>{t(\"language\")}</div>\n <Tabs\n tabs={languages.map((l) => ({ id: l.id, label: l.name }))}\n value={\n languages.find((l) => locale.startsWith(l.id))?.id ??\n languages[0]!.id\n }\n onChange={(l) => void switchLanguage(l)}\n />\n </div>\n ) : null}\n </div>\n\n <WalletsSection />\n\n {features.get(\"tutorials\") ? (\n <Button tone=\"blue\" onClick={() => features.open(\"tutorials\")}>\n <Icon glyph=\"bulb\" size={18} /> {t(\"tutorials\")}\n </Button>\n ) : null}\n <Button tone=\"grey\" onClick={() => setAsk(\"logout\")}>\n <Icon glyph=\"logout\" size={18} /> {t(\"logout\")}\n </Button>\n <Button tone=\"red\" onClick={() => setAsk(\"delete\")}>\n <Icon glyph=\"trash\" size={18} /> {t(\"delete\")}\n </Button>\n\n {renaming ? (\n <RenamePopup\n current={shown}\n onClose={() => setRenaming(false)}\n onDone={(n) => {\n setName(n);\n setRenaming(false);\n }}\n />\n ) : null}\n {ask === \"logout\" ? (\n <ConfirmPopup\n title={t(\"logout\")}\n tone=\"red\"\n onClose={() => setAsk(null)}\n // Only the logout: the host returns to the sign-in screen on \"auth:loggedOut\" by itself.\n // A page reload here blanked the AI Coder's live preview (its files arrive by message\n // and do not survive one) and restarted the whole app for nothing.\n onConfirm={() => client.auth.logout()}\n >\n <div style={{ ...outlined, fontSize: 14, textAlign: \"center\" }}>\n {t(\"logoutAsk\")}\n </div>\n </ConfirmPopup>\n ) : null}\n {ask === \"delete\" ? <DeletePopup onClose={() => setAsk(null)} /> : null}\n </div>\n );\n };\n}\n\nfunction Toggle({\n glyph,\n label,\n value,\n onChange,\n}: {\n glyph: \"sound\" | \"mute\" | \"flame\" | \"globe\";\n label: string;\n value: boolean;\n onChange: (on: boolean) => void;\n}): ReactNode {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={value}\n onClick={() => onChange(!value)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n background: \"none\",\n border: \"none\",\n color: v.text,\n cursor: \"pointer\",\n padding: 0,\n textAlign: \"left\",\n }}\n >\n <Icon glyph={glyph} size={24} />\n <span style={{ flex: 1, ...outlined, fontSize: 14 }}>{label}</span>\n <span\n style={{\n width: 52,\n height: 30,\n borderRadius: 15,\n background: value ? v.green : v.wellSoft,\n boxShadow: `inset 0 0 0 1px ${v.line}`,\n position: \"relative\",\n transition: \"background .2s\",\n }}\n >\n <span\n style={{\n position: \"absolute\",\n top: 3,\n left: value ? 25 : 3,\n width: 24,\n height: 24,\n borderRadius: \"50%\",\n background: \"#fff\",\n transition: \"left .2s\",\n boxShadow: \"0 2px 4px rgba(0,0,0,.3)\",\n }}\n />\n </span>\n </button>\n );\n}\n\n/**\n * Wallets linked by signature, each with \"Unlink\" (`client.auth.unlinkWallet`). Hidden while there are\n * none. The wallet an account signs in with has no button: the server refuses it (`CanUnlink`).\n */\nfunction WalletsSection(): ReactNode {\n const client = useIDosGamesClient();\n const toast = useToast();\n const [wallets, setWallets] = useState<PlayAccessWallet[]>([]);\n const [asking, setAsking] = useState<PlayAccessWallet | null>(null);\n\n const load = useCallback(async () => {\n const res = await client.blockchain.getPlayAccess();\n if (res.ok) setWallets(res.data.LinkedWallets ?? []);\n }, [client]);\n useEffect(() => {\n void load();\n }, [load]);\n\n // The popup shows the spinner and closes itself when this resolves.\n const unlink = async (wallet: PlayAccessWallet) => {\n const res = await client.auth.unlinkWallet(wallet.NetworkID ?? \"\");\n if (!res.ok) {\n const known = unlinkRefusal(res.error);\n toast(known ? t(known) : errorText(res.error), \"error\");\n } else toast(t(\"unlinked\"), \"success\");\n await load();\n };\n\n if (wallets.length === 0) return null;\n return (\n <div style={{ ...panel, padding: 12, display: \"grid\", gap: 10 }}>\n <div style={{ ...outlined, fontSize: 14, color: v.textDim }}>\n {t(\"wallets\")}\n </div>\n {wallets.map((w) => (\n <div\n key={`${w.NetworkID}:${w.Address}`}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n minWidth: 0,\n }}\n >\n <Icon glyph=\"link\" size={22} />\n <div style={{ flex: 1, minWidth: 0, display: \"grid\" }}>\n <span\n style={{\n fontFamily: \"monospace\",\n fontSize: 13,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n }}\n >\n {shortAddress(w.Address)}\n </span>\n <span style={{ ...outlined, fontSize: 11, color: v.textDim }}>\n {(w.NetworkID ?? \"\").toUpperCase()}\n {w.CanUnlink === false ? ` · ${t(\"loginWallet\")}` : \"\"}\n </span>\n </div>\n {w.CanUnlink !== false ? (\n <Button size=\"sm\" tone=\"grey\" onClick={() => setAsking(w)}>\n {t(\"unlink\")}\n </Button>\n ) : null}\n </div>\n ))}\n {asking ? (\n <ConfirmPopup\n title={t(\"unlinkTitle\")}\n confirmLabel={t(\"unlink\")}\n tone=\"red\"\n onClose={() => setAsking(null)}\n onConfirm={() => unlink(asking)}\n >\n <div\n style={{\n fontFamily: \"monospace\",\n fontSize: 14,\n textAlign: \"center\",\n }}\n >\n {shortAddress(asking.Address)}\n </div>\n <div\n style={{\n ...outlined,\n fontSize: 13,\n textAlign: \"center\",\n lineHeight: 1.4,\n }}\n >\n {t(\"unlinkAsk\")}\n </div>\n </ConfirmPopup>\n ) : null}\n </div>\n );\n}\n\nfunction RenamePopup({\n current,\n onClose,\n onDone,\n}: {\n current: string;\n onClose: () => void;\n onDone: (name: string) => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const toast = useToast();\n const [value, setValue] = useState(current);\n const [busy, setBusy] = useState(false);\n const problem = nameProblem(value);\n const save = async () => {\n if (problem) return;\n setBusy(true);\n const res = await client.user.changeUsername(value.trim());\n setBusy(false);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n toast(t(\"renamed\"), \"success\");\n // The SDK does not patch the cached name — the screen shows the server's answer.\n onDone(res.data.Username ?? value.trim());\n };\n return (\n <Popup title={t(\"rename\")} onClose={onClose} width={400}>\n <input\n value={value}\n autoFocus\n maxLength={40}\n onChange={(e) => setValue(e.target.value)}\n onKeyDown={(e) => e.key === \"Enter\" && void save()}\n placeholder={t(\"newName\")}\n style={{\n ...panel,\n padding: \"12px 14px\",\n fontSize: 16,\n color: v.text,\n outline: \"none\",\n }}\n />\n {problem ? (\n <div style={{ ...outlined, fontSize: 12, color: v.red }}>\n {problem === \"short\" ? t(\"nameShort\") : t(\"nameLong\")}\n </div>\n ) : null}\n <Button\n tone=\"green\"\n disabled={!!problem}\n busy={busy}\n onClick={() => void save()}\n >\n {t(\"save\")}\n </Button>\n </Popup>\n );\n}\n\nfunction DeletePopup({ onClose }: { onClose: () => void }): ReactNode {\n const client = useIDosGamesClient();\n const toast = useToast();\n const [typed, setTyped] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const word = t(\"deleteWord\");\n const remove = async () => {\n setBusy(true);\n const res = await client.user.deleteUserAccount();\n setBusy(false);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n // Logout first: it clears the app's notifications, and \"deleted\" must be seen on the\n // sign-in screen the host returns to.\n client.auth.logout();\n toast(t(\"deleted\"));\n };\n return (\n <Popup title={t(\"delete\")} onClose={onClose} width={420}>\n <div style={{ ...outlined, fontSize: 14, textAlign: \"center\" }}>\n {t(\"deleteAsk\")} <span style={{ color: v.red }}>{word}</span>\n </div>\n <input\n value={typed}\n onChange={(e) => setTyped(e.target.value)}\n placeholder={word}\n style={{\n ...panel,\n padding: \"12px 14px\",\n fontSize: 16,\n color: v.text,\n outline: \"none\",\n textAlign: \"center\",\n }}\n />\n <Button\n tone=\"red\"\n disabled={!deleteUnlocked(typed, word)}\n busy={busy}\n onClick={() => void remove()}\n >\n {t(\"delete\")}\n </Button>\n </Popup>\n );\n}\n"
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"path": "module.meta.json",
|
|
67
|
-
"content": "{\n \"id\": \"profile\",\n \"type\": \"feature\",\n \"inBase\": true,\n \"summary\": \"Profile and settings for any game: name and ID, sound, volume and motion, language, tutorials, log out and account deletion.\",\n \"description\": \"A ready profile screen (Unity Alikhan/Username, UI/HUD/Settings, Runtime/UI/Consent, Localization) for ANY game, built on the @idosgames/react/ui kit. The player's name (rename in a popup with the server's rules: 3–24 characters, trimmed) and ID with copy; settings: sound on/off, volume and 'fewer animations' — the kit's player prefs, kept on this device and applied to every screen at once — the game's language (the title's languages, when there are two or more). Buttons: Tutorials (when tutorials is installed), Log out (confirmed) and Delete account — a popup that unlocks only after the player types the confirmation word, then deletes this game's data for the player. Registers the 'profile' feature; the lobby's profile button opens it. No title settings needed.\",\n \"provides\": [\n \"player name and ID\",\n \"rename popup\",\n \"sound, volume and fewer animations\",\n \"language picker\",\n \"log out\",\n \"account deletion with confirmation\"\n ],\n \"tags\": [\n \"profile\",\n \"settings\",\n \"account\",\n \"sound\",\n \"language\",\n \"privacy\",\n \"ui\",\n \"screen\"\n ],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.
|
|
67
|
+
"content": "{\n \"id\": \"profile\",\n \"type\": \"feature\",\n \"inBase\": true,\n \"summary\": \"Profile and settings for any game: name and ID, sound, volume and motion, language, tutorials, log out and account deletion.\",\n \"description\": \"A ready profile screen (Unity Alikhan/Username, UI/HUD/Settings, Runtime/UI/Consent, Localization) for ANY game, built on the @idosgames/react/ui kit. The player's name (rename in a popup with the server's rules: 3–24 characters, trimmed) and ID with copy; settings: sound on/off, volume and 'fewer animations' — the kit's player prefs, kept on this device and applied to every screen at once — the game's language (the title's languages, when there are two or more). Buttons: Tutorials (when tutorials is installed), Log out (confirmed) and Delete account — a popup that unlocks only after the player types the confirmation word, then deletes this game's data for the player. Registers the 'profile' feature; the lobby's profile button opens it. No title settings needed.\",\n \"provides\": [\n \"player name and ID\",\n \"rename popup\",\n \"sound, volume and fewer animations\",\n \"language picker\",\n \"log out\",\n \"account deletion with confirmation\"\n ],\n \"tags\": [\n \"profile\",\n \"settings\",\n \"account\",\n \"sound\",\n \"language\",\n \"privacy\",\n \"ui\",\n \"screen\"\n ],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.2\"\n}\n"
|
|
68
68
|
}
|
|
69
69
|
]
|
|
70
70
|
}
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"version": "0.1.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@idosgames/core": "0.
|
|
37
|
-
"@idosgames/module-sdk": "0.8.
|
|
38
|
-
"@idosgames/react": "0.4.
|
|
36
|
+
"@idosgames/core": "0.20.0",
|
|
37
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
38
|
+
"@idosgames/react": "0.4.2",
|
|
39
39
|
"react": "19.2.7"
|
|
40
40
|
},
|
|
41
41
|
"contentHash": "bafe16c7215f36a5ef659c24f879c07b02fdb5d7e7cedc1cdc4ef996e687879c",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"version": "0.1.0"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@idosgames/core": "0.
|
|
38
|
-
"@idosgames/module-sdk": "0.8.
|
|
39
|
-
"@idosgames/react": "0.4.
|
|
37
|
+
"@idosgames/core": "0.20.0",
|
|
38
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
39
|
+
"@idosgames/react": "0.4.2",
|
|
40
40
|
"react": "19.2.7"
|
|
41
41
|
},
|
|
42
42
|
"contentHash": "7498596d1c779ccadfa57e24b0023652e46c97f5c8657a4ac95ca15b6c8dcc27",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"version": "0.1.1"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@idosgames/core": "0.
|
|
35
|
-
"@idosgames/module-sdk": "0.8.
|
|
36
|
-
"@idosgames/react": "0.4.
|
|
34
|
+
"@idosgames/core": "0.20.0",
|
|
35
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
36
|
+
"@idosgames/react": "0.4.2",
|
|
37
37
|
"react": "19.2.7"
|
|
38
38
|
},
|
|
39
39
|
"contentHash": "e143d6c4a50b6ab0e111a9778dfd7b3cb688f79ef28845242e25455d42e5087b",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"version": "0.1.1"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.
|
|
40
|
-
"@idosgames/module-sdk": "0.8.
|
|
41
|
-
"@idosgames/react": "0.4.
|
|
39
|
+
"@idosgames/core": "0.20.0",
|
|
40
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
41
|
+
"@idosgames/react": "0.4.2",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
44
|
"contentHash": "5d35a1259abfe381fbde61906d79ea6dd44cff932f40e9b91a05f884e0fba349",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"version": "0.1.1"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.
|
|
40
|
-
"@idosgames/module-sdk": "0.8.
|
|
41
|
-
"@idosgames/react": "0.4.
|
|
39
|
+
"@idosgames/core": "0.20.0",
|
|
40
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
41
|
+
"@idosgames/react": "0.4.2",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
44
|
"contentHash": "e5cce64d38d60544b7c512cd693bed6c3f1b289cbb85c7df51408efdceee82fd",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"version": "0.1.1"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@idosgames/core": "0.
|
|
36
|
-
"@idosgames/module-sdk": "0.8.
|
|
37
|
-
"@idosgames/react": "0.4.
|
|
35
|
+
"@idosgames/core": "0.20.0",
|
|
36
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
37
|
+
"@idosgames/react": "0.4.2",
|
|
38
38
|
"react": "19.2.7"
|
|
39
39
|
},
|
|
40
40
|
"contentHash": "cc04f0006881944dfa7119ed02d591b48c7d650dbfdc71f981ecedac5d5998f5",
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
"version": "0.4.1"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@idosgames/core": "0.
|
|
73
|
-
"@idosgames/module-sdk": "0.8.
|
|
72
|
+
"@idosgames/core": "0.20.0",
|
|
73
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
74
74
|
"three": "0.185.1"
|
|
75
75
|
},
|
|
76
76
|
"contentHash": "a4ed25dd430145920db6e2bc98a5b5ec8a0eec78e5f98d7a8ea9be56ed06302f",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"version": "0.1.2"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@idosgames/core": "0.
|
|
43
|
-
"@idosgames/module-sdk": "0.8.
|
|
44
|
-
"@idosgames/react": "0.4.
|
|
42
|
+
"@idosgames/core": "0.20.0",
|
|
43
|
+
"@idosgames/module-sdk": "0.8.1",
|
|
44
|
+
"@idosgames/react": "0.4.2",
|
|
45
45
|
"react": "19.2.7"
|
|
46
46
|
},
|
|
47
47
|
"contentHash": "25a1e02d58fb773f2c26574d6985ffca501001350497cac8f8a067774914e490",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "authentication",
|
|
3
3
|
"description": "Log players into a game on the iDosGames TypeScript SDK (@idosgames/core) via client.auth (AuthenticationService): guest/device-id login, two-step email registration with a confirmation code, email login, Google/Telegram login, SSO-code login from idosgames.com, wallet login, password reset, auto login on relaunch, session refresh, logout, and client-side email/password validation. Use this whenever the user is working in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and asks about logging a player in, sessions, registration, email confirmation codes, resending a code, guest accounts, device-id login, Telegram login, Google login, SSO login, wallet login, forgot/reset password, auto-login, isLoggedIn, or otherwise touches client.auth, AuthenticationService, or AuthContext — even if they don't name the module explicitly.",
|
|
4
|
-
"content": "---\nname: authentication\ndescription: >-\n Log players into a game on the iDosGames TypeScript SDK (@idosgames/core)\n via client.auth (AuthenticationService): guest/device-id login, two-step\n email registration with a confirmation code, email login, Google/Telegram\n login, SSO-code login from idosgames.com, wallet login, password reset, auto\n login on relaunch, session refresh, logout, and client-side email/password\n validation. Use this whenever the user is working in the iDosGames TS SDK or\n its game templates (board-game, idle-rpg) and asks about logging a player\n in, sessions, registration, email confirmation codes, resending a code,\n guest accounts, device-id login, Telegram login, Google login, SSO login,\n wallet login, forgot/reset password, auto-login, isLoggedIn, or otherwise\n touches client.auth, AuthenticationService, or AuthContext — even if they\n don't name the module explicitly.\n---\n\n# Authentication (iDosGames TS SDK)\n\nThe Authentication module is how a player gets a session. It's the one module\nevery other feature depends on: nothing else on `client` works until a login\nmethod has succeeded. Unlike feature modules, most of its methods don't return\na narrow per-feature payload — they return the **entire post-login bootstrap\nstate** (`ClientState`: title config + the player's full `User` state),\nbecause a successful login is also \"give me everything the client needs to\nrender.\"\n\nThis skill is for **using** production login flows, not for porting or\nextending the service. If a login is rejected, that's the backend enforcing a\nrule (bad credentials, duplicate email, banned account) — surface the error,\ndon't try to reproduce the check client-side.\n\n## Remember me\n\n`setRememberSession(remember)` decides whether a successful login is written to storage. Call it\n**before** a `login*` method — it is read when that login completes, and it applies to whichever\nprovider runs next.\n\n```ts\nclient.auth.setRememberSession(rememberCheckbox); // default: true\nconst result = await client.auth.loginWithDeviceID();\n```\n\n- **On** (the default, and how every release before core 0.1.3 behaved) — the session is persisted,\n so `autoLogin()` signs the player back in on the next launch.\n- **Off** — nothing is persisted and any previously remembered session is dropped, so the next\n launch opens on the login screen. The current session is NOT weakened: the credentials stay in\n memory for this tab, so the transport's automatic 401 re-login still works.\n\n`logout()` ends the session and clears the persisted one, so a reload does not walk back into the\naccount the player just left. (Before core 0.1.3 it left storage untouched and a reload silently\nsigned them back in.)\n\nSessions are extended by the **refresh token** (`Authentication/RefreshSession`, core ≥ 0.18): every\nlogin returns one, it is kept in memory and — when remembering — in storage, and rotated on every\nrefresh. A 401 and a cold-start `autoLogin()` use it first, so ANY method comes back, wallet and\nGoogle included; replaying the login method is only the fallback for a missing/expired token. On a\ntitle whose login mode needs a token balance the server re-checks it on that refresh; a balance\nthat dropped closes the game (`client.auth.playAccess.Granted === false`), it does not end the session.\n\n## Mental model\n\n- **Guest login** (`loginWithDeviceID`) — anonymous account keyed off a\n per-device ID the platform adapter generates/stores. Zero-friction first\n launch; no credentials to lose, but also nothing to recover if the device ID\n is gone (reinstall, new device).\n- **Linked login methods** — `loginWithEmail` / `registerWithEmail`,\n `loginWithGoogle`, `loginWithTelegram`, `loginWithSsoCode`. These tie\n the account to a real identity so the player can resume it elsewhere.\n- **`autoLogin()`** picks up where the player left off. It first extends the\n remembered session by its **refresh token** (works for every method). Only\n without a usable token does it fall back to replaying — and that fallback\n covers just the methods that don't need a fresh externally-issued token: it replays `loginWithEmail`\n if the last successful login was Email (saved password), `loginWithDeviceID`\n if it was Device/None, and `loginWithTelegram` if it was Telegram (its\n `initData` is re-read live from the Telegram WebApp bridge each time, not\n stored). For Google/Facebook/GooglePlay/platform-token, there's no stored\n credential to replay — `autoLogin()` returns `reason: \"client\"` instead of\n guessing, because DeviceID and email/platform logins hash to **different\n backend UserIDs** (see Gotchas), so silently falling back to a guest login\n would strand the player on an unrelated empty account. What `autoLogin()`\n has to replay is controlled by **\"remember me\"** — see below. Call this on app\n start instead of hand-rolling \"which method did they use last,\" but be ready\n to handle its failure by re-running the platform's sign-in flow.\n- **Session refresh is automatic.** The HTTP transport calls\n `refreshSession()` itself when a request comes back 401 — it calls the\n server's `RefreshSession` with the refresh token (falling back to replaying\n the login method only without a usable token) and retries once. You almost never call\n `refreshSession()` directly; it's documented here for completeness, not as a\n method you wire up in app code.\n- **Static validators** (`AuthenticationService.isValidEmail`,\n `AuthenticationService.isValidPasswordLength`) are plain synchronous\n functions, not calls to the backend — use them to validate a form before\n spending a network round-trip on a login/register call that will fail\n server-side anyway. They mirror, but do not replace, the server's own\n checks — the server always re-validates.\n\n## Setup\n\n```ts\nimport { createIDosGamesClient } from \"@idosgames/core\";\n\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\nawait client.auth.loginWithDeviceID(); // or any other auth.* login method\n\nclient.auth.isLoggedIn; // true once a login call has succeeded\n```\n\nEvery other module's methods require this to have succeeded first — without a\nsession they return `{ ok: false, reason: \"unauthorized\" }` rather than\nthrowing. There is one `client` per player; don't share it across sessions.\nAuthentication requests carry no Bearer ticket at all — login/register/reset\nare the one family of calls that work before you have a session.\n\n## Methods\n\nAll login/register/reset methods return `Promise<OperationResult<T>>`: either\n`{ ok: true, data }` or `{ ok: false, reason, error }`. Always branch on\n`result.ok` before touching `result.data`. `reason` is one of `\"client\"` (bad\nlocal args, e.g. missing Telegram initData or empty platform token),\n`\"unauthorized\"`, `\"throttled\"` (fired the same endpoint again inside the\nthrottle window), `\"connection\"` (transient, offer Retry), `\"validation\"`\n(response/schema drift), or `\"server\"` (backend rejected it — `error` carries\nthe reason code the backend returned, e.g. `\"INCORRECT_EMAIL_OR_PASSWORD\"`,\n`\"INVALID_VERIFICATION_CODE\"` — see Gotchas for the verbatim set).\n\n| Method | Purpose | `data` on success |\n| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |\n| `loginWithDeviceID()` | Anonymous/guest login keyed off the device ID. | `ClientState` |\n| `loginWithTelegram()` | Login using the Telegram Mini App's init data. | `ClientState` |\n| `loginWithEmail(email, password)` | Login with email + password. | `ClientState` |\n| `registerWithEmail(email, password)` | **Start** a registration. Only mails a code — never returns a session, see below. | `EmailRegistrationOutcome` |\n| `confirmEmailRegistration(email, code)` | Finish the registration with the code from the email, creating the player. | `ClientState` |\n| `resendVerificationCode(email)` | Mail a fresh code. Resolves successfully whatever happened — except when there is no usable sender at all (`EMAIL_SENDER_NOT_CONFIGURED`). | `void` |\n| `requestWalletChallenge(networkID, address)` | Ask for the message the player's wallet must sign. Step one of wallet login. | `WalletChallengeResponse` |\n| `loginWithWallet(networkID, address, signature)` | Exchange the signed challenge for a title session. | `ClientState` |\n| `loginWithGoogle(googleIDToken)` | Login with a Google ID token. | `ClientState` |\n| `loginWithSsoCode(code)` | Exchange a one-time code from idosgames.com for a title session. **Web only** — see below. | `ClientState` |\n| `forgotPassword(email)` | Trigger a password-reset email (a 6-digit code). | `SuccessResponse` |\n| `resetPassword(email, resetToken, password)` | Complete a password reset using the emailed code. | `SuccessResponse` |\n| `autoLogin()` | Replay the last-used auth method (Email/Device/None/Telegram only — fails with `reason: \"client\"` for Google/Facebook/GooglePlay/platform-token). | `ClientState` |\n| `refreshSession()` | Re-authenticate and return a fresh session ticket; used internally by the transport on 401. | `string \\| null` (not an `OperationResult`) |\n| `logout()` | Clear the session, forget the saved login method, and reset the local cache. Synchronous, no network call. | `void` |\n| `setRememberSession(remember)` | \"Remember me\" — whether the NEXT login is persisted for `autoLogin()`. Default `true`. Call before a `login*` method. | `void` |\n| `AuthenticationService.isValidEmail(email)` | Static, synchronous client-side format check. | `boolean` |\n| `AuthenticationService.isValidPasswordLength(pw)` | Static, synchronous length check (8–100 chars). | `boolean` |\n\n⚠ **Each title has its OWN password.** The same address in two games means two independent\npasswords: resetting in one game does not change the login for the other. The reason is that the\nreset e-mail goes out through the PUBLISHER's SMTP — with one shared password, one publisher's mail\nserver would control access to every other publisher's game. So don't tell the player \"the same\npassword as in your other game\", and don't reuse stored credentials across titles.\nThe IDENTITY is still shared: Google, Apple and the idosgames.com sign-in recognise the same person\nin every game. Identity answers \"who is this\", not \"what proves they may sign in\".\n\n⚠ **Signing in through idosgames.com from a NATIVE build is not in this SDK.** `beginSsoRedirect` /\n`loginWithSsoCode` are the web half: the browser comes back to the same page with the code in the\nURL fragment. In a packaged Android/iOS build the browser is a separate program that tells the app\nnothing, so that flow uses a request the game polls for instead (`BeginNativeSsoRequest` /\n`PollNativeSsoRequest`), and it is implemented in the Unity SDK. Don't reach for `loginWithSsoCode`\nfrom a native wrapper — it has no way to receive the code.\n\n⚠ **That native flow now has a SHORT CODE, and a native client should use it.** `BeginNativeSsoRequest`\naccepts `UseUserCode: true` and then answers with `UserCode` (`WDJB-MJHT`) plus `VerificationUri`.\nThe game displays the code; the player opens the plain URL and types it there. The older shape — the\ngame opening `…/sso?title=X&request=<handle>` and the page confirming on its own — still works for\nalready-shipped builds, but **a request created with a code refuses that path**, so a build must do\none or the other, never a mix.\n\nThe reason is not convenience. In the older shape the handle travels IN THE ADDRESS, so the link can\nbe sent to somebody else: the attacker creates the request on their own device, the victim opens the\nlink and confirms, and the attacker's game gets a session as the victim. PKCE does not help (the\nverifier belongs to whoever created the request). With a code there is nothing in the URL to forward\n— the request is named only by what is on the device's own screen. `GetAuthOptions` reports\n`NativeSso.UserCodeSupported` and `NativeSso.PollIntervalSeconds`; ask before assuming, and follow\nthe interval or the poll quota will refuse you.\n\n⚠ **The code is shown, the secret is not.** Players read codes off screens, over shoulders and out\nof screenshots — so the code alone must never buy a session. Polling keeps requiring the request\nsecret, which never leaves the game.\n\nNon-Promise instance getters (read directly, no `await`):\n\n| Getter | Returns |\n| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `client.auth.context` | `AuthContext \\| null` — `{ userID, clientSessionTicket, clientSessionTicketExpiration, platformUserID?, platformAuthToken?, platformAuthTokenExpiration? }`, or `null` if never logged in. |\n| `client.auth.isLoggedIn` | `true` once `context` has a non-empty `userID` and `clientSessionTicket`. |\n| `client.auth.lastAuthType` | The persisted `AuthType` (`\"None\" \\| \"Device\" \\| \"Email\" \\| \"iDosGames\" \\| \"Facebook\" \\| \"Google\" \\| \"GooglePlay\" \\| \"Telegram\"`) from the previous successful login, read from local storage. |\n\nOn a successful login/register call, the SDK mirrors the full `ClientState`\ninto `client.data` (title config + user state) — same cache every other\nmodule reads from — and emits events. `email`/`password` are persisted to\nlocal storage **only** after a successful `loginWithEmail` or\n`confirmEmailRegistration` call, so `autoLogin()` has something to replay.\n⚠ Note it is the CONFIRMATION, not `registerWithEmail`, that persists them:\nregistration alone creates no account, so there is nothing to replay yet (the\npassword is carried over from the first step internally). A leftover\nusername you pass in is only a fallback — Google logins use the Google\nprofile name, platform-token logins use the platform profile name, and if\nnone is available anywhere the backend assigns a generated one (see Gotchas).\n\n## Events\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn.\n\n- `auth:requestSent` → `void` — fired at the start of every login/reset call (before the network round-trip). One exception: `loginWithSsoCode` validates its argument first, so an empty-code `reason: \"client\"` failure doesn't fire it.\n- `auth:loggedIn` → `void` — fired after a login/register call succeeds and `ClientState` has been applied to the cache.\n- `auth:loggedOut` → `void` — fired synchronously by `logout()`.\n- `auth:unauthorized` → `void` — transport-level, fired by the HTTP layer only when a 401 could **not** be transparently recovered (the automatic refresh was unavailable, already attempted, or failed). A 401 that the refresh-and-retry rescues never emits it. Not emitted by `AuthenticationService` itself.\n- `user:clientStateReceived` → `ClientState` — fired whenever a full client state is applied (login, or any other flow that re-fetches it).\n- `user:stateUpdated` → `void` — fired whenever the cached `User` half of `ClientState` is replaced, which includes every successful login.\n- `user:anyUpdated` → `void` — coarsest \"something in the user cache changed\" signal; also fires on login.\n\n```ts\nconst off = client.on(\"auth:loggedIn\", () => {\n console.log(\"logged in as\", client.auth.context?.userID);\n});\n// later: off();\n```\n\n## Recipes\n\n### Guest login on first launch\n\n```ts\nconst result = await client.auth.loginWithDeviceID();\nif (!result.ok) return showError(result.error ?? result.reason);\n// client.data now has the full ClientState; render the game.\n```\n\n### Try auto-login first, fall back appropriately\n\n```ts\nasync function bootstrap() {\n const result = await client.auth.autoLogin();\n if (result.ok) return; // resumed the last session\n if (result.reason === \"client\" && client.auth.lastAuthType !== \"None\") {\n // Last session used Google/Facebook/GooglePlay/a platform token — autoLogin\n // can't replay that silently. Re-run that platform's sign-in flow and call\n // the matching login method (e.g. loginWithGoogle) with a fresh token.\n return reauthenticateViaPlatformSDK(client.auth.lastAuthType);\n }\n showError(result.error ?? result.reason);\n}\n```\n\n`autoLogin()` safely self-replays Email, Device/None, and Telegram — call it\nonce on app start for those. For anything else it deliberately does **not**\nfall back to a guest login (see Gotchas) — treat its `reason: \"client\"`\nfailure as \"go get a fresh token,\" not as a generic error to toast.\n\n### Register a new account with email\n\n```ts\nif (!AuthenticationService.isValidEmail(email)) {\n return showFieldError(\"email\", \"Enter a valid email address.\");\n}\nif (!AuthenticationService.isValidPasswordLength(password)) {\n return showFieldError(\"password\", \"Password must be 8–100 characters.\");\n}\n\nconst started = await client.auth.registerWithEmail(email, password);\nif (!started.ok) return showError(started.error ?? started.reason);\n\n// No account yet — a code was mailed. Show a code screen; the session arrives from confirm.\nconst done = await client.auth.confirmEmailRegistration(email, codeFromPlayer);\nif (!done.ok) return showError(done.error ?? done.reason);\n```\n\n⚠ **`registerWithEmail` never creates an account, and success does not mean the address was\nfree.**\n\nRegistration always stores the attempt and mails a 6-digit code; the player is created by\n`confirmEmailRegistration`. Building a screen that assumes an immediate session will leave players\nstaring at a spinner that never resolves.\n\nConfirmation is **platform behaviour, not a title setting** — there is no flag to switch it off,\nand no `verificationRequired` branch to write. Only the numbers are configurable (code lifetime,\nattempt cap, resend cooldown).\n\nThe answer is also deliberately **identical for a free and for a taken address** — there is no\n`EMAIL_ALREADY_EXISTS` on this path any more. Telling them apart would turn the registration form\ninto a way of checking whether a given person has an account here, which is exactly what the\npassword-recovery form goes out of its way to avoid. The owner of a taken address gets a \"you\nalready have an account\" e-mail instead, without a code.\n\nOne failure on this path **is** reported: `EMAIL_SENDER_NOT_CONFIGURED`, when the server has no\nusable mail sender at all. It says nothing about the address — the same answer comes back for a\nfree one, a taken one and a typo — and it exists because confirmation cannot be switched off: with\nno sender, e-mail registration does not work at all, and answering \"code sent\" would hide a broken\ndeployment behind a normal-looking screen. Show it as a server-side problem, not as the player's\nmistake, and do not offer a \"resend\" — the resend refuses the same way. A letter that merely fails\nto leave is a different case and still answers success.\n\nEvery way of getting the code wrong — no such registration, expired, mismatched — answers with the\nsame `INVALID_VERIFICATION_CODE`, for the same reason. Do not branch on it.\n\n`resendVerificationCode` mails a **new** code: only its hash is stored, so the previous one cannot\nbe re-sent and stops working. Rate-limit the button in the UI (the server's cooldown is reported as\n`resendCooldownSeconds`) — the server answers successfully even when it sent nothing.\n\nEmail is matched case-insensitively — the backend trims and lowercases it\nbefore every lookup and write, so `Player@Mail.com` and `player@mail.com`\ncollide on the same account.\n\n### Upgrade a guest account to an email account\n\nThe SDK has no dedicated \"link email to this guest\" call — `registerWithEmail`\nalways mints its **own** account (a fresh UserID derived from the email, via a\ndifferent hash than the device-id account uses). To move a guest forward\nwithout losing their progress, do it through your own game-side flow (e.g. a\ncloud script that copies/merges state) rather than assuming registration\ncarries the guest's inventory along; don't build a UI that implies it will.\n\n### Forgot / reset password\n\n```ts\nconst sent = await client.auth.forgotPassword(email);\nif (!sent.ok) return showError(sent.error ?? sent.reason);\n// If an account exists for this email, a 6-digit reset code was emailed,\n// valid for 5 minutes. The response looks identical whether or not the\n// email is registered (see Gotchas) — don't tell the player \"no such account.\"\n\nconst reset = await client.auth.resetPassword(email, resetToken, newPassword);\nif (!reset.ok) return showError(reset.error ?? reset.reason);\n// password changed; call loginWithEmail with the new password next\n```\n\n### Handle a rejected login\n\n```ts\nconst result = await client.auth.loginWithEmail(email, password);\nif (!result.ok) {\n switch (result.reason) {\n case \"connection\":\n return offerRetry();\n case \"throttled\":\n return; // ignore — same call already in flight/just ran\n case \"client\":\n case \"server\":\n default:\n return showError(result.error ?? \"Login failed.\");\n }\n}\n```\n\n### Log out\n\n```ts\nclient.auth.logout(); // synchronous — clears context + resets client.data cache\n// route to login screen; no network call is made\n```\n\n## Gotchas\n\n- **A title's login mode can refuse methods** (`cfg.Blockchain.PlayAccess`). `WalletOnly` accepts\n only wallet login (others fail with `PLAY_ACCESS_WALLET_LOGIN_ONLY`); `AnyWithLinkedWallet` refuses\n guests (`PLAY_ACCESS_GUEST_DISABLED`) and lets other logins in with `client.auth.playAccess.Granted\n=== false` until a wallet is linked and holds enough tokens. Read the mode BEFORE login with\n `client.auth.getAuthOptions()` (`PlayAccessMode`) to hide dead buttons. The host template does all\n of this already — see the `blockchain-system` skill, \"Login mode and play access\".\n- **`autoLogin()` deliberately refuses to guess for federated/platform\n logins.** The backend derives DeviceID UserIDs from\n `SHA256(platform + device + deviceID)` and email/platform-linked UserIDs\n from `SHA256(email + titleID)` — two different hashes with no server-side\n link between them. Falling back to `loginWithDeviceID()` for a\n Google/Facebook/GooglePlay/platform-token session would silently resolve to\n a different, empty account, not \"safely resume as a guest.\" If\n `lastAuthType` is one of those, `autoLogin()` returns\n `{ ok: false, reason: \"client\" }` instead; the app must obtain a fresh token\n from that platform's SDK and call the matching `login*` method itself.\n- **The exact `error` strings on `reason: \"server\"` are backend-defined\n codes/messages**, not prose meant for direct display — surface them through\n your own copy/localization layer rather than showing them raw. Verified\n values from the backend for the flows above: `\"INVALID_INPUT_DATA\"`\n (missing/blank required field) and `\"INCORRECT_EMAIL_OR_PASSWORD\"` (bad\n credentials **or** unknown email — the backend intentionally doesn't\n distinguish the two, so don't tell the player \"no such account\") and\n `\"INVALID_VERIFICATION_CODE\"` (every way of getting the confirmation code\n wrong, collapsed into one) are shared\n `MessageCode` enum values also used elsewhere in the backend;\n `\"EMAIL_ALREADY_EXISTS\"` is **no longer returned by registration** — the form\n answers identically for a free and for a taken address, on purpose;\n `\"RATE_LIMIT_EXCEEDED\"` / `\"OPERATION_IN_PROGRESS\"` (per-account login lock\n — see below) are the same enum family. Google/platform-token logins instead\n return ad-hoc string literals specific to that flow, e.g. `\"BANNED_GLOBAL\"`\n (platform account is banned), `\"GOOGLE_ACCOUNT_CONFLICT\"` (email already\n linked to a _different_ Google account), `\"INVALID_GOOGLE_TOKEN\"`\n (bad/expired Google ID token) — see\n [references/data-model.md](references/data-model.md) for the full list.\n Telegram login instead surfaces plain sentences like `\"Invalid Telegram\ndata\"` / an internal `\"Telegram auth_date is stale\"` condition (initData\n older than 24h or timestamped implausibly in the future) — treat any\n non-uppercase-code string as an opaque message, not something to\n pattern-match on.\n- **Per-account login is rate-limited server-side, independent of the SDK's\n own 600 ms throttle.** Repeated `loginWithEmail` attempts for the same\n account inside a ~1-second window come back `\"RATE_LIMIT_EXCEEDED\"`; a login\n already being processed for that account comes back\n `\"OPERATION_IN_PROGRESS\"`. This is a brute-force guard, not a bug — don't\n retry-loop past it.\n- **Telegram login needs the platform adapter's init data.** `loginWithTelegram()`\n calls `platform.getTelegramInitDataRaw()` first; if that returns falsy (not\n running inside Telegram, or the adapter doesn't support it), the call fails\n client-side with `reason: \"client\"` before any network request — no point\n retrying without fixing the environment. Server-side, the init data is\n rejected if its `auth_date` is missing, more than 24 hours old, or more than\n 5 minutes in the future — a Telegram Mini App that's been idle a long time\n may need a fresh WebApp launch, not just a retry. On success it's recorded\n under its own `AuthType.Telegram` (not `Device`) precisely so `autoLogin()`\n replays `loginWithTelegram()` again rather than a device-id login.\n- **`loginWithPlatformToken` no longer exists.** The site used to hand games the\n platform auth token — a 24-hour string that also authorizes the whole\n management API. It was replaced by the one-time SSO code\n (`loginWithSsoCode`), and the endpoint has since been removed as unreachable.\n A session opened by that code is still recorded under `AuthType.iDosGames`,\n which is what `lastAuthType` reports afterward.\n- **A username you pass in is a last resort, not a guarantee.** For Google and\n platform-token logins, the backend prefers the identity provider's own\n profile name over any client-supplied username on every login (not just the\n first) — so a locally cached display name can be silently overwritten by the\n linked account's name. If no name is available from any source at\n registration time, the backend assigns a generated one\n (`\"BraveTiger482\"`-style: adjective + noun + 2–4 digit number) rather than\n leaving it blank.\n- **`forgotPassword` never reveals whether an email is registered.** Unknown\n emails and known emails both return `{ ok: true }` with no email actually\n sent for the unknown case — this is a deliberate anti-enumeration measure,\n not a bug to work around. The emailed reset code is a 6-digit number valid\n for 5 minutes; requesting again inside a 60-second cooldown silently\n no-ops (still returns `ok: true`, no second email). `resetPassword` allows\n at most 5 wrong-code attempts before the code is invalidated server-side and\n a fresh `forgotPassword` call is required.\n ⚠ **The remaining-attempt count is NOT in the response any more**, and neither\n is \"no such account\": every outcome of `resetPassword` — unknown address, no\n code requested, expired code, wrong code — answers `\"INVALID_RESET_CODE\"`, and\n an exhausted code answers `\"RESET_CODE_ATTEMPTS_EXCEEDED\"`. Reporting the\n remaining count both confirmed the account existed and told an attacker how\n much room was left. Don't parse a number out of `error`.\n- **Email credentials are only saved on success.** `loginWithEmail` and\n `confirmEmailRegistration` persist email+password to local storage (scoped by\n `titleID`) only after `fetchAndApplyClientState` succeeds — a failed login\n doesn't overwrite previously saved credentials, and a fresh install has\n nothing saved until the first successful email login. `registerWithEmail` on\n its own saves nothing: at that point the account does not exist yet.\n- **`refreshSession()` has a re-entrancy guard.** If it's already running\n (e.g. two requests 401 at nearly the same time), a second call returns `null`\n immediately instead of recursing — this exists to avoid deadlocking the\n retry, so don't assume a `null` result means the session is actually dead.\n- **`logout()` forgets the saved login method too (changed in core 0.1.3).** It\n nulls the auth context, wipes the cached user state and title-config bundle,\n clears `lastAuthType` and any saved email/password, and emits\n `auth:loggedOut` — so a later `autoLogin()` has nothing to resume. Before\n 0.1.3 storage survived a logout and the next launch silently signed the\n player back into the account they had just left. If you want a sign-out the\n player is remembered through, don't call `logout()`.\n- **`refreshSession()` and `logout()` don't return `OperationResult`.**\n `refreshSession()` resolves to `string | null` (the fresh session ticket, or\n `null` on failure/no saved method); `logout()` is synchronous `void`. Don't\n branch on `.ok` for these two.\n- **`GameLoop` is excluded from the login `ClientState`.** Board/stage state\n and its definitions are fetched separately per-stage by the GameLoop\n feature, not bundled into every login response.\n- **Guard against double-submit.** Each call fires a real request; a\n double-tapped \"Log in\" button can fire twice. Firing the same auth endpoint\n again inside the SDK's own throttle window (default 600 ms) comes back\n `reason: \"throttled\"` rather than duplicating the call, but disable the\n control while a call is in flight rather than relying on that — and note the\n server has its own, stricter per-account lock on top (see above).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — request/response wire\nshapes, the exact backend validation and error-code rules for each login/reset\npath, UserID derivation, and the default-username generator. Read it when you\nneed to reason about _why_ a specific `error` string came back, or when\nbuilding a password-reset or registration UI that needs to match the backend's\ntiming/attempt rules exactly.\n",
|
|
4
|
+
"content": "---\nname: authentication\ndescription: >-\n Log players into a game on the iDosGames TypeScript SDK (@idosgames/core)\n via client.auth (AuthenticationService): guest/device-id login, two-step\n email registration with a confirmation code, email login, Google/Telegram\n login, SSO-code login from idosgames.com, wallet login, password reset, auto\n login on relaunch, session refresh, logout, and client-side email/password\n validation. Use this whenever the user is working in the iDosGames TS SDK or\n its game templates (board-game, idle-rpg) and asks about logging a player\n in, sessions, registration, email confirmation codes, resending a code,\n guest accounts, device-id login, Telegram login, Google login, SSO login,\n wallet login, forgot/reset password, auto-login, isLoggedIn, or otherwise\n touches client.auth, AuthenticationService, or AuthContext — even if they\n don't name the module explicitly.\n---\n\n# Authentication (iDosGames TS SDK)\n\nThe Authentication module is how a player gets a session. It's the one module\nevery other feature depends on: nothing else on `client` works until a login\nmethod has succeeded. Unlike feature modules, most of its methods don't return\na narrow per-feature payload — they return the **entire post-login bootstrap\nstate** (`ClientState`: title config + the player's full `User` state),\nbecause a successful login is also \"give me everything the client needs to\nrender.\"\n\nThis skill is for **using** production login flows, not for porting or\nextending the service. If a login is rejected, that's the backend enforcing a\nrule (bad credentials, duplicate email, banned account) — surface the error,\ndon't try to reproduce the check client-side.\n\n## Remember me\n\n`setRememberSession(remember)` decides whether a successful login is written to storage. Call it\n**before** a `login*` method — it is read when that login completes, and it applies to whichever\nprovider runs next.\n\n```ts\nclient.auth.setRememberSession(rememberCheckbox); // default: true\nconst result = await client.auth.loginWithDeviceID();\n```\n\n- **On** (the default, and how every release before core 0.1.3 behaved) — the session is persisted,\n so `autoLogin()` signs the player back in on the next launch.\n- **Off** — nothing is persisted and any previously remembered session is dropped, so the next\n launch opens on the login screen. The current session is NOT weakened: the credentials stay in\n memory for this tab, so the transport's automatic 401 re-login still works.\n\n`logout()` ends the session and clears the persisted one, so a reload does not walk back into the\naccount the player just left. (Before core 0.1.3 it left storage untouched and a reload silently\nsigned them back in.)\n\nSessions are extended by the **refresh token** (`Authentication/RefreshSession`, core ≥ 0.18): every\nlogin returns one, it is kept in memory and — when remembering — in storage, and rotated on every\nrefresh. A 401 and a cold-start `autoLogin()` use it first, so ANY method comes back, wallet and\nGoogle included; replaying the login method is only the fallback for a missing/expired token. On a\ntitle whose login mode needs a token balance the server re-checks it on that refresh; a balance\nthat dropped closes the game (`client.auth.playAccess.Granted === false`), it does not end the session.\n\n## Mental model\n\n- **Guest login** (`loginWithDeviceID`) — anonymous account keyed off a\n per-device ID the platform adapter generates/stores. Zero-friction first\n launch; no credentials to lose, but also nothing to recover if the device ID\n is gone (reinstall, new device).\n- **Linked login methods** — `loginWithEmail` / `registerWithEmail`,\n `loginWithGoogle`, `loginWithTelegram`, `loginWithSsoCode`. These tie\n the account to a real identity so the player can resume it elsewhere.\n- **`autoLogin()`** picks up where the player left off. It first extends the\n remembered session by its **refresh token** (works for every method). Only\n without a usable token does it fall back to replaying — and that fallback\n covers just the methods that don't need a fresh externally-issued token: it replays `loginWithEmail`\n if the last successful login was Email (saved password), `loginWithDeviceID`\n if it was Device/None, and `loginWithTelegram` if it was Telegram (its\n `initData` is re-read live from the Telegram WebApp bridge each time, not\n stored). For Google/Facebook/GooglePlay/platform-token, there's no stored\n credential to replay — `autoLogin()` returns `reason: \"client\"` instead of\n guessing, because DeviceID and email/platform logins hash to **different\n backend UserIDs** (see Gotchas), so silently falling back to a guest login\n would strand the player on an unrelated empty account. What `autoLogin()`\n has to replay is controlled by **\"remember me\"** — see below. Call this on app\n start instead of hand-rolling \"which method did they use last,\" but be ready\n to handle its failure by re-running the platform's sign-in flow.\n- **Session refresh is automatic.** The HTTP transport calls\n `refreshSession()` itself when a request comes back 401 — it calls the\n server's `RefreshSession` with the refresh token (falling back to replaying\n the login method only without a usable token) and retries once. You almost never call\n `refreshSession()` directly; it's documented here for completeness, not as a\n method you wire up in app code.\n- **Static validators** (`AuthenticationService.isValidEmail`,\n `AuthenticationService.isValidPasswordLength`) are plain synchronous\n functions, not calls to the backend — use them to validate a form before\n spending a network round-trip on a login/register call that will fail\n server-side anyway. They mirror, but do not replace, the server's own\n checks — the server always re-validates.\n\n## Setup\n\n```ts\nimport { createIDosGamesClient } from \"@idosgames/core\";\n\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\nawait client.auth.loginWithDeviceID(); // or any other auth.* login method\n\nclient.auth.isLoggedIn; // true once a login call has succeeded\n```\n\nEvery other module's methods require this to have succeeded first — without a\nsession they return `{ ok: false, reason: \"unauthorized\" }` rather than\nthrowing. There is one `client` per player; don't share it across sessions.\nAuthentication requests carry no Bearer ticket at all — login/register/reset\nare the one family of calls that work before you have a session.\n\n## Methods\n\nAll login/register/reset methods return `Promise<OperationResult<T>>`: either\n`{ ok: true, data }` or `{ ok: false, reason, error }`. Always branch on\n`result.ok` before touching `result.data`. `reason` is one of `\"client\"` (bad\nlocal args, e.g. missing Telegram initData or empty platform token),\n`\"unauthorized\"`, `\"throttled\"` (fired the same endpoint again inside the\nthrottle window), `\"connection\"` (transient, offer Retry), `\"validation\"`\n(response/schema drift), or `\"server\"` (backend rejected it — `error` carries\nthe reason code the backend returned, e.g. `\"INCORRECT_EMAIL_OR_PASSWORD\"`,\n`\"INVALID_VERIFICATION_CODE\"` — see Gotchas for the verbatim set).\n\n| Method | Purpose | `data` on success |\n| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |\n| `loginWithDeviceID()` | Anonymous/guest login keyed off the device ID. | `ClientState` |\n| `loginWithTelegram()` | Login using the Telegram Mini App's init data. | `ClientState` |\n| `loginWithEmail(email, password)` | Login with email + password. | `ClientState` |\n| `registerWithEmail(email, password)` | **Start** a registration. Only mails a code — never returns a session, see below. | `EmailRegistrationOutcome` |\n| `confirmEmailRegistration(email, code)` | Finish the registration with the code from the email, creating the player. | `ClientState` |\n| `resendVerificationCode(email)` | Mail a fresh code. Resolves successfully whatever happened — except when there is no usable sender at all (`EMAIL_SENDER_NOT_CONFIGURED`). | `void` |\n| `requestWalletChallenge(networkID, address)` | Ask for the message the player's wallet must sign. Step one of wallet login. | `WalletChallengeResponse` |\n| `loginWithWallet(networkID, address, signature)` | Exchange the signed challenge for a title session. | `ClientState` |\n| `loginWithGoogle(googleIDToken)` | Login with a Google ID token. | `ClientState` |\n| `loginWithSsoCode(code)` | Exchange a one-time code from idosgames.com for a title session. **Web only** — see below. | `ClientState` |\n| `forgotPassword(email)` | Trigger a password-reset email (a 6-digit code). | `SuccessResponse` |\n| `resetPassword(email, resetToken, password)` | Complete a password reset using the emailed code. | `SuccessResponse` |\n| `autoLogin()` | Replay the last-used auth method (Email/Device/None/Telegram only — fails with `reason: \"client\"` for Google/Facebook/GooglePlay/platform-token). | `ClientState` |\n| `refreshSession()` | Re-authenticate and return a fresh session ticket; used internally by the transport on 401. | `string \\| null` (not an `OperationResult`) |\n| `logout()` | Clear the session, forget the saved login method, and reset the local cache. Synchronous, no network call. | `void` |\n| `setRememberSession(remember)` | \"Remember me\" — whether the NEXT login is persisted for `autoLogin()`. Default `true`. Call before a `login*` method. | `void` |\n| `AuthenticationService.isValidEmail(email)` | Static, synchronous client-side format check. | `boolean` |\n| `AuthenticationService.isValidPasswordLength(pw)` | Static, synchronous length check (8–100 chars). | `boolean` |\n\n⚠ **Each title has its OWN password.** The same address in two games means two independent\npasswords: resetting in one game does not change the login for the other. The reason is that the\nreset e-mail goes out through the PUBLISHER's SMTP — with one shared password, one publisher's mail\nserver would control access to every other publisher's game. So don't tell the player \"the same\npassword as in your other game\", and don't reuse stored credentials across titles.\nThe IDENTITY is still shared: Google, Apple and the idosgames.com sign-in recognise the same person\nin every game. Identity answers \"who is this\", not \"what proves they may sign in\".\n\n⚠ **Signing in through idosgames.com from a NATIVE build is not in this SDK.** `beginSsoRedirect` /\n`loginWithSsoCode` are the web half: the browser comes back to the same page with the code in the\nURL fragment. In a packaged Android/iOS build the browser is a separate program that tells the app\nnothing, so that flow uses a request the game polls for instead (`BeginNativeSsoRequest` /\n`PollNativeSsoRequest`), and it is implemented in the Unity SDK. Don't reach for `loginWithSsoCode`\nfrom a native wrapper — it has no way to receive the code.\n\n⚠ **That native flow now has a SHORT CODE, and a native client should use it.** `BeginNativeSsoRequest`\naccepts `UseUserCode: true` and then answers with `UserCode` (`WDJB-MJHT`) plus `VerificationUri`.\nThe game displays the code; the player opens the plain URL and types it there. The older shape — the\ngame opening `…/sso?title=X&request=<handle>` and the page confirming on its own — still works for\nalready-shipped builds, but **a request created with a code refuses that path**, so a build must do\none or the other, never a mix.\n\nThe reason is not convenience. In the older shape the handle travels IN THE ADDRESS, so the link can\nbe sent to somebody else: the attacker creates the request on their own device, the victim opens the\nlink and confirms, and the attacker's game gets a session as the victim. PKCE does not help (the\nverifier belongs to whoever created the request). With a code there is nothing in the URL to forward\n— the request is named only by what is on the device's own screen. `GetAuthOptions` reports\n`NativeSso.UserCodeSupported` and `NativeSso.PollIntervalSeconds`; ask before assuming, and follow\nthe interval or the poll quota will refuse you.\n\n⚠ **The code is shown, the secret is not.** Players read codes off screens, over shoulders and out\nof screenshots — so the code alone must never buy a session. Polling keeps requiring the request\nsecret, which never leaves the game.\n\nNon-Promise instance getters (read directly, no `await`):\n\n| Getter | Returns |\n| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `client.auth.context` | `AuthContext \\| null` — `{ userID, clientSessionTicket, clientSessionTicketExpiration, platformUserID?, platformAuthToken?, platformAuthTokenExpiration? }`, or `null` if never logged in. |\n| `client.auth.isLoggedIn` | `true` once `context` has a non-empty `userID` and `clientSessionTicket`. |\n| `client.auth.lastAuthType` | The persisted `AuthType` (`\"None\" \\| \"Device\" \\| \"Email\" \\| \"iDosGames\" \\| \"Facebook\" \\| \"Google\" \\| \"GooglePlay\" \\| \"Telegram\"`) from the previous successful login, read from local storage. |\n\nOn a successful login/register call, the SDK mirrors the full `ClientState`\ninto `client.data` (title config + user state) — same cache every other\nmodule reads from — and emits events. `email`/`password` are persisted to\nlocal storage **only** after a successful `loginWithEmail` or\n`confirmEmailRegistration` call, so `autoLogin()` has something to replay.\n⚠ Note it is the CONFIRMATION, not `registerWithEmail`, that persists them:\nregistration alone creates no account, so there is nothing to replay yet (the\npassword is carried over from the first step internally). A leftover\nusername you pass in is only a fallback — Google logins use the Google\nprofile name, platform-token logins use the platform profile name, and if\nnone is available anywhere the backend assigns a generated one (see Gotchas).\n\n## Events\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn.\n\n- `auth:requestSent` → `void` — fired at the start of every login/reset call (before the network round-trip). One exception: `loginWithSsoCode` validates its argument first, so an empty-code `reason: \"client\"` failure doesn't fire it.\n- `auth:loggedIn` → `void` — fired after a login/register call succeeds and `ClientState` has been applied to the cache.\n- `auth:loggedOut` → `void` — fired synchronously by `logout()`.\n- `auth:unauthorized` → `void` — transport-level, fired by the HTTP layer only when a 401 could **not** be transparently recovered (the automatic refresh was unavailable, already attempted, or failed). A 401 that the refresh-and-retry rescues never emits it. Not emitted by `AuthenticationService` itself.\n- `user:clientStateReceived` → `ClientState` — fired whenever a full client state is applied (login, or any other flow that re-fetches it).\n- `user:stateUpdated` → `void` — fired whenever the cached `User` half of `ClientState` is replaced, which includes every successful login.\n- `user:anyUpdated` → `void` — coarsest \"something in the user cache changed\" signal; also fires on login.\n\n```ts\nconst off = client.on(\"auth:loggedIn\", () => {\n console.log(\"logged in as\", client.auth.context?.userID);\n});\n// later: off();\n```\n\n## Recipes\n\n### Guest login on first launch\n\n```ts\nconst result = await client.auth.loginWithDeviceID();\nif (!result.ok) return showError(result.error ?? result.reason);\n// client.data now has the full ClientState; render the game.\n```\n\n### Try auto-login first, fall back appropriately\n\n```ts\nasync function bootstrap() {\n const result = await client.auth.autoLogin();\n if (result.ok) return; // resumed the last session\n if (result.reason === \"client\" && client.auth.lastAuthType !== \"None\") {\n // Last session used Google/Facebook/GooglePlay/a platform token — autoLogin\n // can't replay that silently. Re-run that platform's sign-in flow and call\n // the matching login method (e.g. loginWithGoogle) with a fresh token.\n return reauthenticateViaPlatformSDK(client.auth.lastAuthType);\n }\n showError(result.error ?? result.reason);\n}\n```\n\n`autoLogin()` safely self-replays Email, Device/None, and Telegram — call it\nonce on app start for those. For anything else it deliberately does **not**\nfall back to a guest login (see Gotchas) — treat its `reason: \"client\"`\nfailure as \"go get a fresh token,\" not as a generic error to toast.\n\n### Register a new account with email\n\n```ts\nif (!AuthenticationService.isValidEmail(email)) {\n return showFieldError(\"email\", \"Enter a valid email address.\");\n}\nif (!AuthenticationService.isValidPasswordLength(password)) {\n return showFieldError(\"password\", \"Password must be 8–100 characters.\");\n}\n\nconst started = await client.auth.registerWithEmail(email, password);\nif (!started.ok) return showError(started.error ?? started.reason);\n\n// No account yet — a code was mailed. Show a code screen; the session arrives from confirm.\nconst done = await client.auth.confirmEmailRegistration(email, codeFromPlayer);\nif (!done.ok) return showError(done.error ?? done.reason);\n```\n\n⚠ **`registerWithEmail` never creates an account, and success does not mean the address was\nfree.**\n\nRegistration always stores the attempt and mails a 6-digit code; the player is created by\n`confirmEmailRegistration`. Building a screen that assumes an immediate session will leave players\nstaring at a spinner that never resolves.\n\nConfirmation is **platform behaviour, not a title setting** — there is no flag to switch it off,\nand no `verificationRequired` branch to write. Only the numbers are configurable (code lifetime,\nattempt cap, resend cooldown).\n\nThe answer is also deliberately **identical for a free and for a taken address** — there is no\n`EMAIL_ALREADY_EXISTS` on this path any more. Telling them apart would turn the registration form\ninto a way of checking whether a given person has an account here, which is exactly what the\npassword-recovery form goes out of its way to avoid. The owner of a taken address gets a \"you\nalready have an account\" e-mail instead, without a code.\n\nOne failure on this path **is** reported: `EMAIL_SENDER_NOT_CONFIGURED`, when the server has no\nusable mail sender at all. It says nothing about the address — the same answer comes back for a\nfree one, a taken one and a typo — and it exists because confirmation cannot be switched off: with\nno sender, e-mail registration does not work at all, and answering \"code sent\" would hide a broken\ndeployment behind a normal-looking screen. Show it as a server-side problem, not as the player's\nmistake, and do not offer a \"resend\" — the resend refuses the same way. A letter that merely fails\nto leave is a different case and still answers success.\n\nEvery way of getting the code wrong — no such registration, expired, mismatched — answers with the\nsame `INVALID_VERIFICATION_CODE`, for the same reason. Do not branch on it.\n\n`resendVerificationCode` mails a **new** code: only its hash is stored, so the previous one cannot\nbe re-sent and stops working. Rate-limit the button in the UI (the server's cooldown is reported as\n`resendCooldownSeconds`) — the server answers successfully even when it sent nothing.\n\nEmail is matched case-insensitively — the backend trims and lowercases it\nbefore every lookup and write, so `Player@Mail.com` and `player@mail.com`\ncollide on the same account.\n\n### Upgrade a guest account to an email account\n\nThe SDK has no dedicated \"link email to this guest\" call — `registerWithEmail`\nalways mints its **own** account (a fresh UserID derived from the email, via a\ndifferent hash than the device-id account uses). To move a guest forward\nwithout losing their progress, do it through your own game-side flow (e.g. a\ncloud script that copies/merges state) rather than assuming registration\ncarries the guest's inventory along; don't build a UI that implies it will.\n\n### Forgot / reset password\n\n```ts\nconst sent = await client.auth.forgotPassword(email);\nif (!sent.ok) return showError(sent.error ?? sent.reason);\n// If an account exists for this email, a 6-digit reset code was emailed,\n// valid for 5 minutes. The response looks identical whether or not the\n// email is registered (see Gotchas) — don't tell the player \"no such account.\"\n\nconst reset = await client.auth.resetPassword(email, resetToken, newPassword);\nif (!reset.ok) return showError(reset.error ?? reset.reason);\n// password changed; call loginWithEmail with the new password next\n```\n\n### Handle a rejected login\n\n```ts\nconst result = await client.auth.loginWithEmail(email, password);\nif (!result.ok) {\n switch (result.reason) {\n case \"connection\":\n return offerRetry();\n case \"throttled\":\n return; // ignore — same call already in flight/just ran\n case \"client\":\n case \"server\":\n default:\n return showError(result.error ?? \"Login failed.\");\n }\n}\n```\n\n### Log out\n\n```ts\nclient.auth.logout(); // synchronous — clears context + resets client.data cache\n// route to login screen; no network call is made\n```\n\n## Gotchas\n\n- **A title's login mode can refuse methods** (`cfg.Blockchain.PlayAccess`). `WalletOnly` accepts\n only wallet login (others fail with `PLAY_ACCESS_WALLET_LOGIN_ONLY`); `AnyWithLinkedWallet` refuses\n guests (`PLAY_ACCESS_GUEST_DISABLED`) and lets other logins in with `client.auth.playAccess.Granted\n=== false` until a wallet is linked and holds enough tokens. Read the mode BEFORE login with\n `client.auth.getAuthOptions()` (`PlayAccessMode`) to hide dead buttons. The host template does all\n of this already — see the `blockchain-system` skill, \"Login mode and play access\".\n- **`autoLogin()` deliberately refuses to guess for federated/platform\n logins.** The backend derives DeviceID UserIDs from\n `SHA256(platform + device + deviceID)` and email/platform-linked UserIDs\n from `SHA256(email + titleID)` — two different hashes with no server-side\n link between them. Falling back to `loginWithDeviceID()` for a\n Google/Facebook/GooglePlay/platform-token session would silently resolve to\n a different, empty account, not \"safely resume as a guest.\" If\n `lastAuthType` is one of those, `autoLogin()` returns\n `{ ok: false, reason: \"client\" }` instead; the app must obtain a fresh token\n from that platform's SDK and call the matching `login*` method itself.\n- **The exact `error` strings on `reason: \"server\"` are backend-defined\n codes/messages**, not prose meant for direct display — surface them through\n your own copy/localization layer rather than showing them raw. Verified\n values from the backend for the flows above: `\"INVALID_INPUT_DATA\"`\n (missing/blank required field) and `\"INCORRECT_EMAIL_OR_PASSWORD\"` (bad\n credentials **or** unknown email — the backend intentionally doesn't\n distinguish the two, so don't tell the player \"no such account\") and\n `\"INVALID_VERIFICATION_CODE\"` (every way of getting the confirmation code\n wrong, collapsed into one) are shared\n `MessageCode` enum values also used elsewhere in the backend;\n `\"EMAIL_ALREADY_EXISTS\"` is **no longer returned by registration** — the form\n answers identically for a free and for a taken address, on purpose;\n `\"RATE_LIMIT_EXCEEDED\"` / `\"OPERATION_IN_PROGRESS\"` (per-account login lock\n — see below) are the same enum family. Google/platform-token logins instead\n return ad-hoc string literals specific to that flow, e.g. `\"BANNED_GLOBAL\"`\n (platform account is banned), `\"GOOGLE_ACCOUNT_CONFLICT\"` (email already\n linked to a _different_ Google account), `\"INVALID_GOOGLE_TOKEN\"`\n (bad/expired Google ID token) — see\n [references/data-model.md](references/data-model.md) for the full list.\n Telegram login instead surfaces plain sentences like `\"Invalid Telegram\ndata\"` / an internal `\"Telegram auth_date is stale\"` condition (initData\n older than 24h or timestamped implausibly in the future) — treat any\n non-uppercase-code string as an opaque message, not something to\n pattern-match on.\n- **Per-account login is rate-limited server-side, independent of the SDK's\n own 600 ms throttle.** Repeated `loginWithEmail` attempts for the same\n account inside a ~1-second window come back `\"RATE_LIMIT_EXCEEDED\"`; a login\n already being processed for that account comes back\n `\"OPERATION_IN_PROGRESS\"`. This is a brute-force guard, not a bug — don't\n retry-loop past it.\n- **Telegram login needs the platform adapter's init data.** `loginWithTelegram()`\n calls `platform.getTelegramInitDataRaw()` first; if that returns falsy (not\n running inside Telegram, or the adapter doesn't support it), the call fails\n client-side with `reason: \"client\"` before any network request — no point\n retrying without fixing the environment. Server-side, the init data is\n rejected if its `auth_date` is missing, more than 24 hours old, or more than\n 5 minutes in the future — a Telegram Mini App that's been idle a long time\n may need a fresh WebApp launch, not just a retry. On success it's recorded\n under its own `AuthType.Telegram` (not `Device`) precisely so `autoLogin()`\n replays `loginWithTelegram()` again rather than a device-id login.\n- **`loginWithPlatformToken` no longer exists.** The site used to hand games the\n platform auth token — a 24-hour string that also authorizes the whole\n management API. It was replaced by the one-time SSO code\n (`loginWithSsoCode`), and the endpoint has since been removed as unreachable.\n A session opened by that code is still recorded under `AuthType.iDosGames`,\n which is what `lastAuthType` reports afterward.\n- **A username you pass in is a last resort, not a guarantee.** For Google and\n platform-token logins, the backend prefers the identity provider's own\n profile name over any client-supplied username on every login (not just the\n first) — so a locally cached display name can be silently overwritten by the\n linked account's name. If no name is available from any source at\n registration time, the backend assigns a generated one\n (`\"BraveTiger482\"`-style: adjective + noun + 2–4 digit number) rather than\n leaving it blank.\n- **`forgotPassword` never reveals whether an email is registered.** Unknown\n emails and known emails both return `{ ok: true }` with no email actually\n sent for the unknown case — this is a deliberate anti-enumeration measure,\n not a bug to work around. The emailed reset code is a 6-digit number valid\n for 5 minutes; requesting again inside a 60-second cooldown silently\n no-ops (still returns `ok: true`, no second email). `resetPassword` allows\n at most 5 wrong-code attempts before the code is invalidated server-side and\n a fresh `forgotPassword` call is required.\n ⚠ **The remaining-attempt count is NOT in the response any more**, and neither\n is \"no such account\": every outcome of `resetPassword` — unknown address, no\n code requested, expired code, wrong code — answers `\"INVALID_RESET_CODE\"`, and\n an exhausted code answers `\"RESET_CODE_ATTEMPTS_EXCEEDED\"`. Reporting the\n remaining count both confirmed the account existed and told an attacker how\n much room was left. Don't parse a number out of `error`.\n- **Email credentials are only saved on success.** `loginWithEmail` and\n `confirmEmailRegistration` persist email+password to local storage (scoped by\n `titleID`) only after `fetchAndApplyClientState` succeeds — a failed login\n doesn't overwrite previously saved credentials, and a fresh install has\n nothing saved until the first successful email login. `registerWithEmail` on\n its own saves nothing: at that point the account does not exist yet.\n- **`refreshSession()` has a re-entrancy guard.** If it's already running\n (e.g. two requests 401 at nearly the same time), a second call returns `null`\n immediately instead of recursing — this exists to avoid deadlocking the\n retry, so don't assume a `null` result means the session is actually dead.\n- **`logout()` forgets the saved login method too (changed in core 0.1.3).** It\n nulls the auth context, wipes the cached user state and title-config bundle,\n clears `lastAuthType` and any saved email/password, and emits\n `auth:loggedOut` — so a later `autoLogin()` has nothing to resume. Before\n 0.1.3 storage survived a logout and the next launch silently signed the\n player back into the account they had just left. If you want a sign-out the\n player is remembered through, don't call `logout()`.\n- **Never reload the page after `logout()`** (or after deleting the account). The host shell\n (`AuthGate`) returns to the sign-in screen by itself on `auth:loggedOut`. A `location.reload()`\n restarts the whole app for nothing and, in the AI Coder's live preview, left a dark empty frame:\n the preview's files arrive by message and do not survive a reload.\n- **`refreshSession()` and `logout()` don't return `OperationResult`.**\n `refreshSession()` resolves to `string | null` (the fresh session ticket, or\n `null` on failure/no saved method); `logout()` is synchronous `void`. Don't\n branch on `.ok` for these two.\n- **`GameLoop` is excluded from the login `ClientState`.** Board/stage state\n and its definitions are fetched separately per-stage by the GameLoop\n feature, not bundled into every login response.\n- **Guard against double-submit.** Each call fires a real request; a\n double-tapped \"Log in\" button can fire twice. Firing the same auth endpoint\n again inside the SDK's own throttle window (default 600 ms) comes back\n `reason: \"throttled\"` rather than duplicating the call, but disable the\n control while a call is in flight rather than relying on that — and note the\n server has its own, stricter per-account lock on top (see above).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — request/response wire\nshapes, the exact backend validation and error-code rules for each login/reset\npath, UserID derivation, and the default-username generator. Read it when you\nneed to reason about _why_ a specific `error` string came back, or when\nbuilding a password-reset or registration UI that needs to match the backend's\ntiming/attempt rules exactly.\n",
|
|
5
5
|
"references": [
|
|
6
6
|
{
|
|
7
7
|
"path": "data-model.md",
|