@dcl-regenesislabs/web-editor 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/editor.css +1 -0
- package/editor.js +33891 -0
- package/package.json +8 -4
- package/prefabs/admin-tools/composite.json +77 -0
- package/prefabs/admin-tools/data.json +28 -0
- package/prefabs/admin-tools/icons/admin-panel-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-moderation-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-rewards-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-smart-item-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-text-announcement-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-panel-video-control-button.png +0 -0
- package/prefabs/admin-tools/icons/admin-tool-background.png +0 -0
- package/prefabs/admin-tools/icons/announcements/text-announcement-chat-message.png +0 -0
- package/prefabs/admin-tools/icons/announcements/text-announcement-check.png +0 -0
- package/prefabs/admin-tools/icons/announcements/text-announcement-close-button.png +0 -0
- package/prefabs/admin-tools/icons/moderation/admin-panel-verified-user.png +0 -0
- package/prefabs/admin-tools/icons/moderation/ban.png +0 -0
- package/prefabs/admin-tools/icons/moderation/chevron-back.png +0 -0
- package/prefabs/admin-tools/icons/moderation/chevron-forward.png +0 -0
- package/prefabs/admin-tools/icons/moderation/close.png +0 -0
- package/prefabs/admin-tools/icons/moderation/error.png +0 -0
- package/prefabs/admin-tools/icons/moderation/person-outline.png +0 -0
- package/prefabs/admin-tools/icons/moderation-control-icon.png +0 -0
- package/prefabs/admin-tools/icons/rewards/close.png +0 -0
- package/prefabs/admin-tools/icons/rewards/send.png +0 -0
- package/prefabs/admin-tools/icons/rewards-control.png +0 -0
- package/prefabs/admin-tools/icons/smart-item-control.png +0 -0
- package/prefabs/admin-tools/icons/text-announcement-control.png +0 -0
- package/prefabs/admin-tools/icons/video/chevron-down.png +0 -0
- package/prefabs/admin-tools/icons/video/chevron-up.png +0 -0
- package/prefabs/admin-tools/icons/video/copy-to-clipboard.png +0 -0
- package/prefabs/admin-tools/icons/video/error.png +0 -0
- package/prefabs/admin-tools/icons/video/eye-off.png +0 -0
- package/prefabs/admin-tools/icons/video/eye.png +0 -0
- package/prefabs/admin-tools/icons/video/help.png +0 -0
- package/prefabs/admin-tools/icons/video/info.png +0 -0
- package/prefabs/admin-tools/icons/video/person-outline.png +0 -0
- package/prefabs/admin-tools/icons/video/star.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-dcl-cast.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-live.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-loop.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-mute.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-pause-button.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-play-button.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-video-icon.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-volume-minus-button.png +0 -0
- package/prefabs/admin-tools/icons/video/video-control-volume-plus-button.png +0 -0
- package/prefabs/admin-tools/icons/video-control.png +0 -0
- package/prefabs/admin-tools/scripts/actions.ts +206 -0
- package/prefabs/admin-tools/scripts/admin.tsx +253 -0
- package/prefabs/admin-tools/scripts/api.ts +125 -0
- package/prefabs/admin-tools/scripts/components.ts +190 -0
- package/prefabs/admin-tools/scripts/icons.ts +53 -0
- package/prefabs/admin-tools/scripts/message-bus.ts +297 -0
- package/prefabs/admin-tools/scripts/state.ts +69 -0
- package/prefabs/admin-tools/scripts/tabs/Moderation.tsx +155 -0
- package/prefabs/admin-tools/scripts/tabs/Rewards.tsx +136 -0
- package/prefabs/admin-tools/scripts/tabs/SmartItemActions.tsx +167 -0
- package/prefabs/admin-tools/scripts/tabs/TextAnnouncements.tsx +195 -0
- package/prefabs/admin-tools/scripts/tabs/VideoControl.tsx +156 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/Overlay.tsx +94 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/icons.ts +15 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/state.ts +71 -0
- package/prefabs/admin-tools/scripts/tabs/announcements/ui.tsx +37 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/AddUserInput.tsx +184 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/RemoveAdminConfirmation.tsx +93 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/UsersList.tsx +228 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/api.ts +35 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/icons.ts +23 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/kick.ts +95 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/state.ts +69 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/ui.tsx +172 -0
- package/prefabs/admin-tools/scripts/tabs/moderation/utils.ts +88 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/Button.tsx +52 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/Captcha.tsx +88 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/api.ts +138 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/claim.ts +99 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/controller.ts +103 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/icons.ts +13 -0
- package/prefabs/admin-tools/scripts/tabs/rewards/state.ts +32 -0
- package/prefabs/admin-tools/scripts/tabs/smart-items/selection.ts +90 -0
- package/prefabs/admin-tools/scripts/tabs/video/DclCast.tsx +226 -0
- package/prefabs/admin-tools/scripts/tabs/video/LiveStream.tsx +284 -0
- package/prefabs/admin-tools/scripts/tabs/video/SpeakerShowcase.tsx +112 -0
- package/prefabs/admin-tools/scripts/tabs/video/VideoUrl.tsx +107 -0
- package/prefabs/admin-tools/scripts/tabs/video/Volume.tsx +69 -0
- package/prefabs/admin-tools/scripts/tabs/video/api.ts +172 -0
- package/prefabs/admin-tools/scripts/tabs/video/controls.ts +108 -0
- package/prefabs/admin-tools/scripts/tabs/video/icons.ts +59 -0
- package/prefabs/admin-tools/scripts/tabs/video/speakers.ts +43 -0
- package/prefabs/admin-tools/scripts/tabs/video/state.ts +82 -0
- package/prefabs/admin-tools/scripts/tabs/video/ui.tsx +243 -0
- package/prefabs/admin-tools/scripts/types.ts +46 -0
- package/prefabs/admin-tools/scripts/ui.tsx +122 -0
- package/prefabs/admin-tools/thumbnail.png +0 -0
- package/prefabs/announcer/ai.md +76 -0
- package/prefabs/announcer/composite.json +31 -0
- package/prefabs/announcer/data.json +35 -0
- package/prefabs/announcer/scripts/announcer.tsx +79 -0
- package/prefabs/announcer/scripts/pure/toast.ts +30 -0
- package/prefabs/announcer/thumbnail.png +0 -0
- package/prefabs/black-chair/HighBackChair.glb +0 -0
- package/prefabs/black-chair/composite.json +74 -0
- package/prefabs/black-chair/data.json +30 -0
- package/prefabs/black-chair/scripts/seat.ts +191 -0
- package/prefabs/black-chair/thumbnail.png +0 -0
- package/prefabs/catalog.json +1 -0
- package/prefabs/classic-bench/Bench_01.glb +0 -0
- package/prefabs/classic-bench/composite.json +100 -0
- package/prefabs/classic-bench/data.json +34 -0
- package/prefabs/classic-bench/file1.png +0 -0
- package/prefabs/classic-bench/scripts/seat.ts +191 -0
- package/prefabs/classic-bench/thumbnail.png +0 -0
- package/prefabs/classic-bench-armrests/Bench 1.glb +0 -0
- package/prefabs/classic-bench-armrests/composite.json +100 -0
- package/prefabs/classic-bench-armrests/data.json +30 -0
- package/prefabs/classic-bench-armrests/scripts/seat.ts +191 -0
- package/prefabs/classic-bench-armrests/thumbnail.png +0 -0
- package/prefabs/classroom-chair/Chair_02.glb +0 -0
- package/prefabs/classroom-chair/composite.json +74 -0
- package/prefabs/classroom-chair/data.json +34 -0
- package/prefabs/classroom-chair/file1.png +0 -0
- package/prefabs/classroom-chair/scripts/seat.ts +191 -0
- package/prefabs/classroom-chair/thumbnail.png +0 -0
- package/prefabs/court-chair/Chair_Wood_01.glb +0 -0
- package/prefabs/court-chair/FanstasyPack_TX.png +0 -0
- package/prefabs/court-chair/composite.json +74 -0
- package/prefabs/court-chair/data.json +34 -0
- package/prefabs/court-chair/scripts/seat.ts +191 -0
- package/prefabs/court-chair/thumbnail.png +0 -0
- package/prefabs/curved-couch/CurvedCouch.glb +0 -0
- package/prefabs/curved-couch/composite.json +100 -0
- package/prefabs/curved-couch/data.json +30 -0
- package/prefabs/curved-couch/scripts/seat.ts +191 -0
- package/prefabs/curved-couch/thumbnail.png +0 -0
- package/prefabs/game-flow/ai.md +94 -0
- package/prefabs/game-flow/composite.json +45 -0
- package/prefabs/game-flow/data.json +27 -0
- package/prefabs/game-flow/scripts/game-flow.ts +168 -0
- package/prefabs/game-flow/scripts/pure/flow.ts +185 -0
- package/prefabs/game-flow/thumbnail.png +0 -0
- package/prefabs/health-respawn/ai.md +76 -0
- package/prefabs/health-respawn/composite.json +31 -0
- package/prefabs/health-respawn/data.json +38 -0
- package/prefabs/health-respawn/scripts/health-respawn.ts +98 -0
- package/prefabs/health-respawn/scripts/health.ts +22 -0
- package/prefabs/health-respawn/scripts/pure/health.ts +60 -0
- package/prefabs/health-respawn/thumbnail.png +0 -0
- package/prefabs/high-stool/HighStool.glb +0 -0
- package/prefabs/high-stool/composite.json +74 -0
- package/prefabs/high-stool/data.json +30 -0
- package/prefabs/high-stool/scripts/seat.ts +191 -0
- package/prefabs/high-stool/thumbnail.png +0 -0
- package/prefabs/large-couch/ThreeSeater_Couch.glb +0 -0
- package/prefabs/large-couch/composite.json +100 -0
- package/prefabs/large-couch/data.json +30 -0
- package/prefabs/large-couch/scripts/seat.ts +191 -0
- package/prefabs/large-couch/thumbnail.png +0 -0
- package/prefabs/leaderboard/ai.md +75 -0
- package/prefabs/leaderboard/board-panel.glb +0 -0
- package/prefabs/leaderboard/composite.json +82 -0
- package/prefabs/leaderboard/data.json +35 -0
- package/prefabs/leaderboard/scripts/leaderboard.ts +66 -0
- package/prefabs/leaderboard/scripts/pure/board.ts +71 -0
- package/prefabs/leaderboard/thumbnail.png +0 -0
- package/prefabs/loveseat/Couch_01.glb +0 -0
- package/prefabs/loveseat/composite.json +100 -0
- package/prefabs/loveseat/data.json +34 -0
- package/prefabs/loveseat/file1.png +0 -0
- package/prefabs/loveseat/scripts/seat.ts +191 -0
- package/prefabs/loveseat/thumbnail.png +0 -0
- package/prefabs/modern-sofa/ModernSofa.glb +0 -0
- package/prefabs/modern-sofa/composite.json +100 -0
- package/prefabs/modern-sofa/data.json +30 -0
- package/prefabs/modern-sofa/scripts/seat.ts +191 -0
- package/prefabs/modern-sofa/thumbnail.png +0 -0
- package/prefabs/outdoor-chair/Chair_07.glb +0 -0
- package/prefabs/outdoor-chair/composite.json +74 -0
- package/prefabs/outdoor-chair/data.json +34 -0
- package/prefabs/outdoor-chair/file1.png +0 -0
- package/prefabs/outdoor-chair/scripts/seat.ts +191 -0
- package/prefabs/outdoor-chair/thumbnail.png +0 -0
- package/prefabs/rustic-bench/Bench_01.glb +0 -0
- package/prefabs/rustic-bench/PiratesPack_TX.png.png +0 -0
- package/prefabs/rustic-bench/composite.json +100 -0
- package/prefabs/rustic-bench/data.json +34 -0
- package/prefabs/rustic-bench/scripts/seat.ts +191 -0
- package/prefabs/rustic-bench/thumbnail.png +0 -0
- package/prefabs/rustic-round-stool/Chairwood_01.glb +0 -0
- package/prefabs/rustic-round-stool/PiratesPack_TX.png.png +0 -0
- package/prefabs/rustic-round-stool/composite.json +74 -0
- package/prefabs/rustic-round-stool/data.json +34 -0
- package/prefabs/rustic-round-stool/scripts/seat.ts +191 -0
- package/prefabs/rustic-round-stool/thumbnail.png +0 -0
- package/prefabs/rustic-square-stool/Chairwood_02.glb +0 -0
- package/prefabs/rustic-square-stool/PiratesPack_TX.png.png +0 -0
- package/prefabs/rustic-square-stool/composite.json +74 -0
- package/prefabs/rustic-square-stool/data.json +34 -0
- package/prefabs/rustic-square-stool/scripts/seat.ts +191 -0
- package/prefabs/rustic-square-stool/thumbnail.png +0 -0
- package/prefabs/server-clock/ai.md +67 -0
- package/prefabs/server-clock/composite.json +45 -0
- package/prefabs/server-clock/data.json +24 -0
- package/prefabs/server-clock/scripts/clock-hud.tsx +55 -0
- package/prefabs/server-clock/scripts/server-clock.ts +71 -0
- package/prefabs/server-clock/thumbnail.png +0 -0
- package/prefabs/simple-chair/Chair_03.glb +0 -0
- package/prefabs/simple-chair/composite.json +74 -0
- package/prefabs/simple-chair/data.json +34 -0
- package/prefabs/simple-chair/file1.png +0 -0
- package/prefabs/simple-chair/scripts/seat.ts +191 -0
- package/prefabs/simple-chair/thumbnail.png +0 -0
- package/prefabs/sit-spot/composite.json +45 -0
- package/prefabs/sit-spot/data.json +30 -0
- package/prefabs/sit-spot/scripts/seat.ts +191 -0
- package/prefabs/sit-spot/thumbnail.png +0 -0
- package/prefabs/sit-spot-edge/composite.json +42 -0
- package/prefabs/sit-spot-edge/data.json +32 -0
- package/prefabs/sit-spot-edge/emotes/Sit_Edge1_emote.glb +0 -0
- package/prefabs/sit-spot-edge/emotes/Sit_Edge2_emote.glb +0 -0
- package/prefabs/sit-spot-edge/scripts/seat.ts +191 -0
- package/prefabs/sit-spot-edge/thumbnail.png +0 -0
- package/prefabs/small-couch/TwoSeater_Couch.glb +0 -0
- package/prefabs/small-couch/composite.json +100 -0
- package/prefabs/small-couch/data.json +30 -0
- package/prefabs/small-couch/scripts/seat.ts +191 -0
- package/prefabs/small-couch/thumbnail.png +0 -0
- package/prefabs/spawner/ai.md +109 -0
- package/prefabs/spawner/composite.json +73 -0
- package/prefabs/spawner/data.json +22 -0
- package/prefabs/spawner/scripts/spawner.ts +380 -0
- package/prefabs/spawner/thumbnail.png +0 -0
- package/prefabs/steampunk-bench/Steampunk Bench.glb +0 -0
- package/prefabs/steampunk-bench/composite.json +100 -0
- package/prefabs/steampunk-bench/data.json +30 -0
- package/prefabs/steampunk-bench/scripts/seat.ts +191 -0
- package/prefabs/steampunk-bench/thumbnail.png +0 -0
- package/prefabs/steampunk-chair/Steampunk Chair.glb +0 -0
- package/prefabs/steampunk-chair/composite.json +74 -0
- package/prefabs/steampunk-chair/data.json +30 -0
- package/prefabs/steampunk-chair/scripts/seat.ts +191 -0
- package/prefabs/steampunk-chair/thumbnail.png +0 -0
- package/prefabs/tall-stool/Stool_D.glb +0 -0
- package/prefabs/tall-stool/composite.json +74 -0
- package/prefabs/tall-stool/data.json +30 -0
- package/prefabs/tall-stool/scripts/seat.ts +191 -0
- package/prefabs/tall-stool/thumbnail.png +0 -0
- package/prefabs/trigger-zone/ai.md +115 -0
- package/prefabs/trigger-zone/composite.json +52 -0
- package/prefabs/trigger-zone/data.json +31 -0
- package/prefabs/trigger-zone/scripts/trigger-zone.ts +89 -0
- package/prefabs/trigger-zone/thumbnail.png +0 -0
- package/prefabs/video-screen/composite.json +98 -0
- package/prefabs/video-screen/data.json +21 -0
- package/prefabs/video-screen/thumbnail.png +0 -0
- package/prefabs/video-screen/video_player.glb +0 -0
- package/prefabs/wooden-chair/Chair 1.glb +0 -0
- package/prefabs/wooden-chair/composite.json +74 -0
- package/prefabs/wooden-chair/data.json +30 -0
- package/prefabs/wooden-chair/scripts/seat.ts +191 -0
- package/prefabs/wooden-chair/thumbnail.png +0 -0
- package/prefabs/wooden-chair-armrests/Chair 3.glb +0 -0
- package/prefabs/wooden-chair-armrests/composite.json +74 -0
- package/prefabs/wooden-chair-armrests/data.json +30 -0
- package/prefabs/wooden-chair-armrests/scripts/seat.ts +191 -0
- package/prefabs/wooden-chair-armrests/thumbnail.png +0 -0
- package/scene/about +17 -0
- package/scene/bafkreias2w4xuqaic6sdcvblrisayi6xg7drfowzpt5ubjkmrk3mx6czc4 +1 -0
- package/scene/bafkreiby2gmthtauf7cckttphmhpecsueyypi2hzlke4lcodg7fm4cm2sq +23 -0
- package/scene/bafkreidhwqwajqk4nsuffef5fkdrgmp5ql6vnn2knpqsuqalyosv5o6lk4 +1 -0
- package/scene/bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku +0 -0
- package/scene/bafybeihw3xlbff4sf5sc3kvmlmlwf2visnty5lfymawlunzt5vrkp5fd7u +90 -0
- package/web-build/esbuild.wasm +0 -0
- package/web-build/index.js +3 -0
- package/web-build/sdk-snapshot.json +1 -0
- package/web-build/worker.js +701 -0
- package/README.md +0 -3
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// View state for the moderation tab. It lives at module scope rather than in
|
|
2
|
+
// ReactEcs.useState because the shell unmounts the tab body on every tab switch
|
|
3
|
+
// (state.ts nextTick trick) — a list the admin paged into would otherwise reset.
|
|
4
|
+
// Same reason the Creator Hub keeps `moderationControlState` outside the tree.
|
|
5
|
+
import type { SceneAdmin } from '../../api'
|
|
6
|
+
|
|
7
|
+
export enum ModerationView {
|
|
8
|
+
MAIN = 'main',
|
|
9
|
+
ADMIN_LIST = 'admin_list',
|
|
10
|
+
BAN_LIST = 'ban_list',
|
|
11
|
+
CONFIRM_REMOVE = 'confirm_remove'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const USERS_PER_PAGE = 4
|
|
15
|
+
const MESSAGE_MS = 3000
|
|
16
|
+
|
|
17
|
+
export interface ModerationViewState {
|
|
18
|
+
view: ModerationView
|
|
19
|
+
page: number
|
|
20
|
+
adminToRemove: SceneAdmin | null
|
|
21
|
+
removeError: string
|
|
22
|
+
removing: boolean
|
|
23
|
+
message: string
|
|
24
|
+
messageAt: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const moderationView: ModerationViewState = {
|
|
28
|
+
view: ModerationView.MAIN,
|
|
29
|
+
page: 1,
|
|
30
|
+
adminToRemove: null,
|
|
31
|
+
removeError: '',
|
|
32
|
+
removing: false,
|
|
33
|
+
message: '',
|
|
34
|
+
messageAt: 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function openView(view: ModerationView): void {
|
|
38
|
+
moderationView.view = view
|
|
39
|
+
moderationView.page = 1
|
|
40
|
+
moderationView.adminToRemove = null
|
|
41
|
+
moderationView.removeError = ''
|
|
42
|
+
moderationView.removing = false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function showMessage(text: string): void {
|
|
46
|
+
moderationView.message = text
|
|
47
|
+
moderationView.messageAt = Date.now()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Read during render, so the toast disappears on its own without a timer system.
|
|
51
|
+
export function currentMessage(): string {
|
|
52
|
+
if (moderationView.message === '') return ''
|
|
53
|
+
if (Date.now() - moderationView.messageAt > MESSAGE_MS) {
|
|
54
|
+
moderationView.message = ''
|
|
55
|
+
return ''
|
|
56
|
+
}
|
|
57
|
+
return moderationView.message
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function pageCount(total: number): number {
|
|
61
|
+
return Math.max(1, Math.ceil(total / USERS_PER_PAGE))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function clampPage(total: number): number {
|
|
65
|
+
const pages = pageCount(total)
|
|
66
|
+
if (moderationView.page > pages) moderationView.page = pages
|
|
67
|
+
if (moderationView.page < 1) moderationView.page = 1
|
|
68
|
+
return moderationView.page
|
|
69
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import ReactEcs, { Label, UiEntity } from '@dcl/sdk/react-ecs'
|
|
2
|
+
import type { Key, UiTransformProps } from '@dcl/sdk/react-ecs'
|
|
3
|
+
import { Color4 } from '@dcl/sdk/math'
|
|
4
|
+
|
|
5
|
+
export const WHITE = Color4.White()
|
|
6
|
+
export const BLACK = Color4.Black()
|
|
7
|
+
export const GRAY = Color4.Gray()
|
|
8
|
+
export const ADDRESS_GRAY = Color4.fromHexString('#716B7C')
|
|
9
|
+
export const DIVIDER_GRAY = Color4.fromHexString('#43404A')
|
|
10
|
+
export const DISABLED_GRAY = Color4.fromHexString('#323232')
|
|
11
|
+
export const BADGE_GRAY = Color4.fromHexString('#A09BA8')
|
|
12
|
+
export const DANGER = Color4.fromHexString('#FF2D55')
|
|
13
|
+
export const BAN_PINK = Color4.fromHexString('#FB3B3B')
|
|
14
|
+
export const RED = Color4.Red()
|
|
15
|
+
|
|
16
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'text'
|
|
17
|
+
|
|
18
|
+
export function Divider(props: { uiTransform?: UiTransformProps }): ReactEcs.JSX.Element {
|
|
19
|
+
return (
|
|
20
|
+
<UiEntity
|
|
21
|
+
uiTransform={{
|
|
22
|
+
width: '100%',
|
|
23
|
+
height: 1,
|
|
24
|
+
flexShrink: 0,
|
|
25
|
+
margin: { top: 14, bottom: 14 },
|
|
26
|
+
...props.uiTransform
|
|
27
|
+
}}
|
|
28
|
+
uiBackground={{ color: DIVIDER_GRAY }}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function SectionTitle(props: { text: string }): ReactEcs.JSX.Element {
|
|
34
|
+
return (
|
|
35
|
+
<Label
|
|
36
|
+
value={`<b>${props.text}</b>`}
|
|
37
|
+
fontSize={17}
|
|
38
|
+
color={WHITE}
|
|
39
|
+
textAlign="middle-left"
|
|
40
|
+
uiTransform={{ height: 'auto', margin: { bottom: 8 } }}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ErrorLine(props: {
|
|
46
|
+
icon: string
|
|
47
|
+
text: string
|
|
48
|
+
uiTransform?: UiTransformProps
|
|
49
|
+
}): ReactEcs.JSX.Element {
|
|
50
|
+
return (
|
|
51
|
+
<UiEntity
|
|
52
|
+
uiTransform={{
|
|
53
|
+
width: '100%',
|
|
54
|
+
height: 'auto',
|
|
55
|
+
flexDirection: 'row',
|
|
56
|
+
alignItems: 'center',
|
|
57
|
+
margin: { top: 6 },
|
|
58
|
+
...props.uiTransform
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<UiEntity
|
|
62
|
+
uiTransform={{ width: 16, height: 16, flexShrink: 0, margin: { right: 8 } }}
|
|
63
|
+
uiBackground={{ textureMode: 'stretch', texture: { src: props.icon } }}
|
|
64
|
+
/>
|
|
65
|
+
<UiEntity
|
|
66
|
+
uiTransform={{ width: '90%', height: 'auto' }}
|
|
67
|
+
uiText={{ value: props.text, fontSize: 13, color: RED, textAlign: 'top-left' }}
|
|
68
|
+
/>
|
|
69
|
+
</UiEntity>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function backgroundColor(variant: ButtonVariant, disabled: boolean): Color4 {
|
|
74
|
+
if (disabled) return variant === 'primary' ? DISABLED_GRAY : Color4.create(0, 0, 0, 0)
|
|
75
|
+
if (variant === 'primary') return WHITE
|
|
76
|
+
if (variant === 'danger') return BAN_PINK
|
|
77
|
+
return Color4.create(0, 0, 0, 0)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function labelColor(variant: ButtonVariant, disabled: boolean): Color4 {
|
|
81
|
+
if (disabled) return DISABLED_GRAY
|
|
82
|
+
if (variant === 'primary') return BLACK
|
|
83
|
+
return WHITE
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function Button(props: {
|
|
87
|
+
key?: Key
|
|
88
|
+
label: string
|
|
89
|
+
variant?: ButtonVariant
|
|
90
|
+
disabled?: boolean
|
|
91
|
+
icon?: string
|
|
92
|
+
iconRight?: string
|
|
93
|
+
color?: Color4
|
|
94
|
+
fontSize?: number
|
|
95
|
+
onClick: () => void
|
|
96
|
+
uiTransform?: UiTransformProps
|
|
97
|
+
}): ReactEcs.JSX.Element {
|
|
98
|
+
const variant = props.variant ?? 'primary'
|
|
99
|
+
const disabled = props.disabled === true
|
|
100
|
+
const color = disabled ? DISABLED_GRAY : (props.color ?? labelColor(variant, disabled))
|
|
101
|
+
return (
|
|
102
|
+
<UiEntity
|
|
103
|
+
uiTransform={{
|
|
104
|
+
height: 38,
|
|
105
|
+
flexDirection: 'row',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
justifyContent: 'center',
|
|
108
|
+
borderRadius: 10,
|
|
109
|
+
borderWidth: variant === 'secondary' ? 2 : 0,
|
|
110
|
+
borderColor: disabled ? DISABLED_GRAY : WHITE,
|
|
111
|
+
padding: { left: 12, right: 12 },
|
|
112
|
+
...props.uiTransform
|
|
113
|
+
}}
|
|
114
|
+
uiBackground={{ color: backgroundColor(variant, disabled) }}
|
|
115
|
+
onMouseDown={() => {
|
|
116
|
+
if (!disabled) props.onClick()
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{props.icon === undefined ? null : (
|
|
120
|
+
<UiEntity
|
|
121
|
+
uiTransform={{ width: 20, height: 20, flexShrink: 0, margin: { right: 8 } }}
|
|
122
|
+
uiBackground={{ textureMode: 'stretch', texture: { src: props.icon }, color }}
|
|
123
|
+
/>
|
|
124
|
+
)}
|
|
125
|
+
<Label value={`<b>${props.label}</b>`} fontSize={props.fontSize ?? 15} color={color} />
|
|
126
|
+
{props.iconRight === undefined ? null : (
|
|
127
|
+
<UiEntity
|
|
128
|
+
uiTransform={{ width: 20, height: 20, flexShrink: 0, margin: { left: 8 } }}
|
|
129
|
+
uiBackground={{ textureMode: 'stretch', texture: { src: props.iconRight }, color }}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
</UiEntity>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function ListHeader(props: {
|
|
137
|
+
icon: string
|
|
138
|
+
title: string
|
|
139
|
+
counter: string
|
|
140
|
+
closeIcon: string
|
|
141
|
+
onClose: () => void
|
|
142
|
+
}): ReactEcs.JSX.Element {
|
|
143
|
+
return (
|
|
144
|
+
<UiEntity
|
|
145
|
+
uiTransform={{
|
|
146
|
+
width: '100%',
|
|
147
|
+
height: 32,
|
|
148
|
+
flexDirection: 'row',
|
|
149
|
+
alignItems: 'center',
|
|
150
|
+
margin: { bottom: 10 }
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<UiEntity
|
|
154
|
+
uiTransform={{ width: 26, height: 26, flexShrink: 0, margin: { right: 8 } }}
|
|
155
|
+
uiBackground={{ textureMode: 'stretch', texture: { src: props.icon }, color: WHITE }}
|
|
156
|
+
/>
|
|
157
|
+
<Label value={`<b>${props.title}</b>`} fontSize={19} color={WHITE} />
|
|
158
|
+
<Label
|
|
159
|
+
value={props.counter}
|
|
160
|
+
fontSize={14}
|
|
161
|
+
color={GRAY}
|
|
162
|
+
uiTransform={{ flexGrow: 1, margin: { left: 8 } }}
|
|
163
|
+
textAlign="middle-left"
|
|
164
|
+
/>
|
|
165
|
+
<UiEntity
|
|
166
|
+
uiTransform={{ width: 26, height: 26, flexShrink: 0 }}
|
|
167
|
+
uiBackground={{ textureMode: 'stretch', texture: { src: props.closeIcon }, color: WHITE }}
|
|
168
|
+
onMouseDown={props.onClose}
|
|
169
|
+
/>
|
|
170
|
+
</UiEntity>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Pure-ish helpers behind the moderation tab: input validation, the two list
|
|
2
|
+
// refreshes and the permission rule for `allowNonOwnersManageAdminAllowList`.
|
|
3
|
+
import {
|
|
4
|
+
getSceneAdmins,
|
|
5
|
+
getSceneBans,
|
|
6
|
+
isPreview,
|
|
7
|
+
toSceneAdmins,
|
|
8
|
+
type SceneAdmin,
|
|
9
|
+
type SceneBanUser
|
|
10
|
+
} from '../../api'
|
|
11
|
+
import type { AdminToolsValue } from '../../components'
|
|
12
|
+
import type { AdminMessageBus } from '../../message-bus'
|
|
13
|
+
import type { AdminState } from '../../state'
|
|
14
|
+
import type { AdminPlayer } from '../../types'
|
|
15
|
+
|
|
16
|
+
const ADDRESS = /^0x[a-fA-F0-9]{40}$/
|
|
17
|
+
|
|
18
|
+
export function isValidAddress(value: string): boolean {
|
|
19
|
+
return ADDRESS.test(value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A Decentraland NAME is at most 15 characters, so anything longer was meant to be
|
|
23
|
+
// a wallet address — that is how the Hub decides which request shape to send.
|
|
24
|
+
export function looksLikeAddress(value: string): boolean {
|
|
25
|
+
return value.length > 15
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function shortAddress(address: string): string {
|
|
29
|
+
if (address.length <= 12) return address
|
|
30
|
+
return `${address.slice(0, 6)}...${address.slice(-4)}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function addressOf(user: SceneAdmin | SceneBanUser): string {
|
|
34
|
+
return 'address' in user ? user.address : user.bannedAddress
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The gatekeeper omits `name` for wallets with no NAME, so this never trusts it.
|
|
38
|
+
export function userName(user: SceneAdmin | SceneBanUser): string {
|
|
39
|
+
return typeof user.name === 'string' ? user.name : ''
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function displayName(user: SceneAdmin | SceneBanUser): string {
|
|
43
|
+
const name = userName(user)
|
|
44
|
+
return name === '' ? shortAddress(addressOf(user)) : name
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isAdminUser(admins: SceneAdmin[], value: string): boolean {
|
|
48
|
+
const lower = value.toLowerCase()
|
|
49
|
+
return admins.some(
|
|
50
|
+
(admin) => admin.address === lower || (lower !== '' && userName(admin).toLowerCase() === lower)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The gatekeeper marks scene owners and operators as non-removable, which is the
|
|
55
|
+
// only signal the /scene-admin response carries about who is more than an admin.
|
|
56
|
+
export function isSceneOwner(admins: SceneAdmin[], address: string | undefined): boolean {
|
|
57
|
+
if (address === undefined) return false
|
|
58
|
+
const lower = address.toLowerCase()
|
|
59
|
+
return admins.some((admin) => admin.address === lower && !admin.canBeRemoved)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function canManageAdmins(
|
|
63
|
+
config: AdminToolsValue,
|
|
64
|
+
admins: SceneAdmin[],
|
|
65
|
+
player: AdminPlayer | null
|
|
66
|
+
): boolean {
|
|
67
|
+
if (config.moderationControl.allowNonOwnersManageAdminAllowList) return true
|
|
68
|
+
if (isPreview()) return true
|
|
69
|
+
return isSceneOwner(admins, player?.userId)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// `broadcast` tells the other clients to refetch too — only worth the comms message
|
|
73
|
+
// after the list actually changed, which is how the Hub splits its two fetchers.
|
|
74
|
+
export async function refreshAdmins(
|
|
75
|
+
state: AdminState,
|
|
76
|
+
bus: AdminMessageBus | null,
|
|
77
|
+
broadcast = false
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const [error, response] = await getSceneAdmins()
|
|
80
|
+
state.admins = error === null ? toSceneAdmins(response) : []
|
|
81
|
+
bus?.updateAdminList(state.admins)
|
|
82
|
+
if (broadcast) bus?.syncAdmins()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function refreshBans(state: AdminState): Promise<void> {
|
|
86
|
+
const [error, response] = await getSceneBans()
|
|
87
|
+
state.bans = error === null && Array.isArray(response.results) ? response.results : []
|
|
88
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import ReactEcs, { Label, UiEntity } from '@dcl/sdk/react-ecs'
|
|
2
|
+
import type { UiTransformProps } from '@dcl/sdk/react-ecs'
|
|
3
|
+
import { Color4 } from '@dcl/sdk/math'
|
|
4
|
+
|
|
5
|
+
const ACTIVE = Color4.create(252 / 255, 252 / 255, 252 / 255, 1)
|
|
6
|
+
const DISABLED = Color4.create(74 / 255, 74 / 255, 74 / 255, 1)
|
|
7
|
+
const DISABLED_TEXT = Color4.create(160 / 255, 155 / 255, 168 / 255, 1)
|
|
8
|
+
|
|
9
|
+
export function RewardsButton(props: {
|
|
10
|
+
label: string
|
|
11
|
+
icon?: string
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
onClick: () => void
|
|
14
|
+
uiTransform?: UiTransformProps
|
|
15
|
+
}): ReactEcs.JSX.Element {
|
|
16
|
+
const disabled = props.disabled === true
|
|
17
|
+
return (
|
|
18
|
+
<UiEntity
|
|
19
|
+
uiTransform={{
|
|
20
|
+
height: 40,
|
|
21
|
+
flexDirection: 'row',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
justifyContent: 'center',
|
|
24
|
+
borderRadius: 12,
|
|
25
|
+
borderWidth: 0,
|
|
26
|
+
padding: { left: 16, right: 16 },
|
|
27
|
+
margin: { right: 16 },
|
|
28
|
+
...props.uiTransform
|
|
29
|
+
}}
|
|
30
|
+
uiBackground={{ color: disabled ? DISABLED : ACTIVE }}
|
|
31
|
+
onMouseDown={() => {
|
|
32
|
+
if (!disabled) props.onClick()
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{props.icon !== undefined ? (
|
|
36
|
+
<UiEntity
|
|
37
|
+
uiTransform={{ width: 25, height: 25, margin: { right: 8 } }}
|
|
38
|
+
uiBackground={{
|
|
39
|
+
textureMode: 'stretch',
|
|
40
|
+
texture: { src: props.icon },
|
|
41
|
+
color: disabled ? DISABLED_TEXT : Color4.Black()
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
) : null}
|
|
45
|
+
<Label
|
|
46
|
+
value={`<b>${props.label}</b>`}
|
|
47
|
+
fontSize={16}
|
|
48
|
+
color={disabled ? DISABLED_TEXT : Color4.Black()}
|
|
49
|
+
/>
|
|
50
|
+
</UiEntity>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import ReactEcs, { Input, Label, UiEntity } from '@dcl/sdk/react-ecs'
|
|
2
|
+
import { Color4 } from '@dcl/sdk/math'
|
|
3
|
+
import { TEXT_DIM } from '../../ui'
|
|
4
|
+
import { RewardsButton } from './Button'
|
|
5
|
+
import type { RewardCaptcha } from './api'
|
|
6
|
+
|
|
7
|
+
export function CaptchaPrompt(props: {
|
|
8
|
+
captcha: RewardCaptcha
|
|
9
|
+
answer: string
|
|
10
|
+
busy: boolean
|
|
11
|
+
closeIcon: string
|
|
12
|
+
onChange: (value: string) => void
|
|
13
|
+
onSubmit: () => void
|
|
14
|
+
onClose: () => void
|
|
15
|
+
}): ReactEcs.JSX.Element {
|
|
16
|
+
return (
|
|
17
|
+
<UiEntity uiTransform={{ width: '100%', flexDirection: 'column' }}>
|
|
18
|
+
<UiEntity
|
|
19
|
+
uiTransform={{
|
|
20
|
+
width: '100%',
|
|
21
|
+
flexDirection: 'row',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
margin: { bottom: 16 }
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<Label
|
|
27
|
+
value="<b>Solve the captcha</b>"
|
|
28
|
+
fontSize={16}
|
|
29
|
+
color={Color4.White()}
|
|
30
|
+
textAlign="middle-left"
|
|
31
|
+
uiTransform={{ flexGrow: 1 }}
|
|
32
|
+
/>
|
|
33
|
+
<UiEntity
|
|
34
|
+
uiTransform={{ width: 24, height: 24 }}
|
|
35
|
+
uiBackground={{
|
|
36
|
+
textureMode: 'stretch',
|
|
37
|
+
texture: { src: props.closeIcon },
|
|
38
|
+
color: Color4.White()
|
|
39
|
+
}}
|
|
40
|
+
onMouseDown={props.onClose}
|
|
41
|
+
/>
|
|
42
|
+
</UiEntity>
|
|
43
|
+
|
|
44
|
+
<UiEntity
|
|
45
|
+
uiTransform={{
|
|
46
|
+
width: '100%',
|
|
47
|
+
height: 120,
|
|
48
|
+
margin: { bottom: 16 },
|
|
49
|
+
borderRadius: 8
|
|
50
|
+
}}
|
|
51
|
+
uiBackground={{
|
|
52
|
+
color: Color4.White(),
|
|
53
|
+
textureMode: 'stretch',
|
|
54
|
+
texture: { src: props.captcha.image }
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
<Input
|
|
59
|
+
key={`captcha-${props.captcha.id}`}
|
|
60
|
+
placeholder="Enter the captcha"
|
|
61
|
+
value={props.answer}
|
|
62
|
+
fontSize={16}
|
|
63
|
+
textAlign="middle-left"
|
|
64
|
+
color={Color4.Black()}
|
|
65
|
+
placeholderColor={Color4.create(0.4, 0.4, 0.4, 1)}
|
|
66
|
+
uiTransform={{ width: '100%', height: 40, margin: { bottom: 16 } }}
|
|
67
|
+
uiBackground={{ color: Color4.White() }}
|
|
68
|
+
onChange={props.onChange}
|
|
69
|
+
onSubmit={props.onSubmit}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<UiEntity uiTransform={{ flexDirection: 'row' }}>
|
|
73
|
+
<RewardsButton
|
|
74
|
+
label={props.busy ? 'Sending…' : 'Submit'}
|
|
75
|
+
disabled={props.busy}
|
|
76
|
+
onClick={props.onSubmit}
|
|
77
|
+
/>
|
|
78
|
+
<Label
|
|
79
|
+
value="The reward is assigned once the captcha is accepted."
|
|
80
|
+
fontSize={12}
|
|
81
|
+
color={TEXT_DIM}
|
|
82
|
+
textAlign="middle-left"
|
|
83
|
+
uiTransform={{ width: 220, height: 'auto' }}
|
|
84
|
+
/>
|
|
85
|
+
</UiEntity>
|
|
86
|
+
</UiEntity>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// rewards.decentraland.<domain> calls behind the CLAIM_AIRDROP flow, ported from
|
|
2
|
+
// @dcl/asset-packs src/actions.ts (fetchCampaignsByDispenserKey / fetchCaptcha /
|
|
3
|
+
// requestToken). Every response comes wrapped in an `{ ok, data }` envelope that
|
|
4
|
+
// the shared signedFetch helper does not unwrap, so it is narrowed here.
|
|
5
|
+
import { ENDPOINTS, request, type Result } from '../../api'
|
|
6
|
+
|
|
7
|
+
export interface RewardCampaign {
|
|
8
|
+
campaignId: string
|
|
9
|
+
enabled: boolean
|
|
10
|
+
requiresCaptcha: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RewardCaptcha {
|
|
14
|
+
id: string
|
|
15
|
+
image: string
|
|
16
|
+
width: number
|
|
17
|
+
height: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ClaimSummary {
|
|
21
|
+
status: string
|
|
22
|
+
count: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ClaimRequest {
|
|
26
|
+
dispenserKey: string
|
|
27
|
+
beneficiary: string
|
|
28
|
+
catalyst: string
|
|
29
|
+
captchaId?: string
|
|
30
|
+
captchaValue?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const JSON_HEADERS = { 'Content-Type': 'application/json' }
|
|
34
|
+
|
|
35
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
36
|
+
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function asString(value: unknown, fallback = ''): string {
|
|
40
|
+
return typeof value === 'string' ? value : fallback
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function asNumber(value: unknown, fallback: number): number {
|
|
44
|
+
return typeof value === 'number' && !Number.isNaN(value) ? value : fallback
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function asBoolean(value: unknown, fallback: boolean): boolean {
|
|
48
|
+
return typeof value === 'boolean' ? value : fallback
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function envelopeError(json: Record<string, unknown>): string {
|
|
52
|
+
const message = asString(json.error) !== '' ? asString(json.error) : asString(json.message)
|
|
53
|
+
if (message !== '') return message
|
|
54
|
+
const code = asString(json.code)
|
|
55
|
+
return code !== '' ? code : 'the rewards server rejected the request'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function unwrap(body: unknown): Result<unknown> {
|
|
59
|
+
const json = asRecord(body)
|
|
60
|
+
if (json.ok === false) return [envelopeError(json), null]
|
|
61
|
+
return [null, json.data]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toCampaign(value: unknown): RewardCampaign | null {
|
|
65
|
+
const json = asRecord(value)
|
|
66
|
+
const campaignId = asString(json.campaign_id)
|
|
67
|
+
if (campaignId === '') return null
|
|
68
|
+
return {
|
|
69
|
+
campaignId,
|
|
70
|
+
enabled: asBoolean(json.enabled, true),
|
|
71
|
+
requiresCaptcha: asBoolean(json.requires_captcha, false)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function fetchCampaigns(dispenserKey: string): Promise<Result<RewardCampaign[]>> {
|
|
76
|
+
const url = `${ENDPOINTS.rewardsCampaignKeys()}?campaign_key=${encodeURIComponent(dispenserKey)}`
|
|
77
|
+
const [failure, body] = await request<unknown>({ url })
|
|
78
|
+
if (failure !== null) return [failure, null]
|
|
79
|
+
const [invalid, data] = unwrap(body)
|
|
80
|
+
if (invalid !== null) return [invalid, null]
|
|
81
|
+
if (!Array.isArray(data)) return ['the rewards server returned no campaigns for this key', null]
|
|
82
|
+
const campaigns: RewardCampaign[] = []
|
|
83
|
+
for (const entry of data) {
|
|
84
|
+
const campaign = toCampaign(entry)
|
|
85
|
+
if (campaign !== null) campaigns.push(campaign)
|
|
86
|
+
}
|
|
87
|
+
return [null, campaigns]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function fetchCaptcha(): Promise<Result<RewardCaptcha>> {
|
|
91
|
+
const [failure, body] = await request<unknown>({
|
|
92
|
+
url: ENDPOINTS.rewardsCaptcha(),
|
|
93
|
+
init: { method: 'POST', headers: JSON_HEADERS }
|
|
94
|
+
})
|
|
95
|
+
if (failure !== null) return [failure, null]
|
|
96
|
+
const [invalid, data] = unwrap(body)
|
|
97
|
+
if (invalid !== null) return [invalid, null]
|
|
98
|
+
const json = asRecord(data)
|
|
99
|
+
const id = asString(json.id)
|
|
100
|
+
const image = asString(json.image)
|
|
101
|
+
if (id === '' || image === '') return ['the captcha could not be loaded, try again', null]
|
|
102
|
+
return [null, { id, image, width: asNumber(json.width, 250), height: asNumber(json.height, 100) }]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toSummary(data: unknown): ClaimSummary {
|
|
106
|
+
if (Array.isArray(data)) {
|
|
107
|
+
const first = asRecord(data[0])
|
|
108
|
+
return { status: asString(first.status, 'assigned'), count: data.length }
|
|
109
|
+
}
|
|
110
|
+
const json = asRecord(data)
|
|
111
|
+
return { status: asString(json.status, 'assigned'), count: Object.keys(json).length > 0 ? 1 : 0 }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** True when the server is asking for a captcha rather than reporting a real failure. */
|
|
115
|
+
export function isCaptchaRequired(message: string): boolean {
|
|
116
|
+
return message.toLowerCase().includes('captcha')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function claimReward(claim: ClaimRequest): Promise<Result<ClaimSummary>> {
|
|
120
|
+
const body: Record<string, string> = {
|
|
121
|
+
campaign_key: claim.dispenserKey,
|
|
122
|
+
beneficiary: claim.beneficiary,
|
|
123
|
+
catalyst: claim.catalyst
|
|
124
|
+
}
|
|
125
|
+
if (claim.captchaId !== undefined && claim.captchaValue !== undefined) {
|
|
126
|
+
body.captcha_id = claim.captchaId
|
|
127
|
+
body.captcha_value = claim.captchaValue
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const [failure, response] = await request<unknown>({
|
|
131
|
+
url: ENDPOINTS.rewardsAssign(),
|
|
132
|
+
init: { method: 'POST', headers: JSON_HEADERS, body: JSON.stringify(body) }
|
|
133
|
+
})
|
|
134
|
+
if (failure !== null) return [failure, null]
|
|
135
|
+
const [invalid, data] = unwrap(response)
|
|
136
|
+
if (invalid !== null) return [invalid, null]
|
|
137
|
+
return [null, toSummary(data)]
|
|
138
|
+
}
|