@stripchatdev/create-stripchat-extension 0.1.46 → 0.1.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli/actions/updateAction.d.ts +6 -0
- package/dist/bin/cli/actions/updateAction.js +33 -0
- package/dist/bin/cli/helpers/printCliError.d.ts +1 -0
- package/dist/bin/cli/helpers/printCliError.js +21 -0
- package/dist/bin/cli/index.js +70 -0
- package/dist/bin/create.d.ts +2 -0
- package/dist/bin/create.js +246 -0
- package/dist/package.json +9 -4
- package/dist/src/constants.d.ts +14 -9
- package/dist/src/constants.js +73 -12
- package/dist/src/dependencies/constants.d.ts +2 -0
- package/dist/src/dependencies/constants.js +11 -0
- package/dist/src/dependencies/resolveDependencies.d.ts +6 -0
- package/dist/src/dependencies/resolveDependencies.js +43 -0
- package/dist/src/dependencies/resolveDependencies.test.js +74 -0
- package/dist/src/helpers/error.d.ts +3 -0
- package/dist/src/helpers/error.js +9 -0
- package/dist/src/helpers/error.test.js +15 -0
- package/dist/src/helpers/getIsDirectoryExists.d.ts +7 -0
- package/dist/src/helpers/getIsDirectoryExists.js +23 -0
- package/dist/src/helpers/getIsDirectoryExists.test.js +34 -0
- package/dist/src/helpers/getIsJsxFramework.d.ts +2 -0
- package/dist/src/helpers/getIsJsxFramework.js +2 -0
- package/dist/src/helpers/getIsJsxFramework.test.js +10 -0
- package/dist/src/helpers/getIsTypescript.d.ts +2 -0
- package/dist/src/helpers/getIsTypescript.js +1 -0
- package/dist/src/helpers/getIsTypescript.test.js +10 -0
- package/dist/src/helpers/isSubPath.d.ts +1 -0
- package/dist/src/helpers/isSubPath.js +5 -0
- package/dist/src/helpers/normalizePath.d.ts +2 -0
- package/dist/src/helpers/normalizePath.js +4 -0
- package/dist/src/helpers/normalizePath.test.js +19 -0
- package/dist/src/migrator/executor.d.ts +9 -0
- package/dist/src/migrator/executor.js +112 -0
- package/dist/src/migrator/migration.d.ts +3 -0
- package/dist/src/migrator/migration.js +18 -0
- package/dist/src/migrator/migration.test.js +74 -0
- package/dist/src/migrator/migrations/migrations.test.js +97 -0
- package/dist/src/migrator/migrations/v2/AGENTS_md/001_recreate-with-generated-part.d.ts +2 -0
- package/dist/src/migrator/migrations/v2/AGENTS_md/001_recreate-with-generated-part.js +14 -0
- package/dist/src/migrator/migrations/v2/_project-config_json/001_create.d.ts +2 -0
- package/dist/src/migrator/migrations/v2/_project-config_json/001_create.js +14 -0
- package/dist/src/migrator/migrations/v2/vite_config_ts/001_turn_off_minify_and_assets_inline.d.ts +2 -0
- package/dist/src/migrator/migrations/v2/vite_config_ts/001_turn_off_minify_and_assets_inline.js +118 -0
- package/dist/src/migrator/planner.d.ts +5 -0
- package/dist/src/migrator/planner.js +65 -0
- package/dist/src/migrator/planner.test.js +40 -0
- package/dist/src/migrator/types.d.ts +29 -0
- package/dist/src/migrator/updates/updates.test.js +102 -0
- package/dist/src/migrator/updates/v2/AGENTS_md/001_update-generated-part.d.ts +2 -0
- package/dist/src/migrator/updates/v2/AGENTS_md/001_update-generated-part.js +38 -0
- package/dist/src/migrator/updates/v2/package_json/001_update-versions.d.ts +2 -0
- package/dist/src/migrator/updates/v2/package_json/001_update-versions.js +77 -0
- package/dist/src/migrator/updates/v2/package_json/002_add-cli-commands.d.ts +2 -0
- package/dist/src/migrator/updates/v2/package_json/002_add-cli-commands.js +61 -0
- package/dist/src/migrator/validators/getIsValidMigration.d.ts +2 -0
- package/dist/src/migrator/validators/getIsValidMigration.js +3 -0
- package/dist/src/migrator/validators/getIsValidMigration.test.js +13 -0
- package/dist/src/projectConfig/buildProjectConfig.d.ts +15 -0
- package/dist/src/projectConfig/buildProjectConfig.js +27 -0
- package/dist/src/projectConfig/detectPackageManagerRuntime.d.ts +2 -0
- package/dist/src/{helpers/detectPackageManager.js → projectConfig/detectPackageManagerRuntime.js} +1 -1
- package/dist/src/{helpers/detectPackageManager.test.js → projectConfig/detectPackageManagerRuntime.test.js} +10 -10
- package/dist/src/projectConfig/recoverProjectConfig.d.ts +1 -0
- package/dist/src/projectConfig/recoverProjectConfig.js +34 -0
- package/dist/src/projectConfig/recovery/detectPackageManagerFromFiles.d.ts +2 -0
- package/dist/src/projectConfig/recovery/detectPackageManagerFromFiles.js +16 -0
- package/dist/src/projectConfig/recovery/detectPackageManagerFromFiles.test.d.ts +1 -0
- package/dist/src/projectConfig/recovery/detectPackageManagerFromFiles.test.js +37 -0
- package/dist/src/projectConfig/recovery/parseAgentsMd.d.ts +10 -0
- package/dist/src/projectConfig/recovery/parseAgentsMd.js +52 -0
- package/dist/src/projectConfig/recovery/parseAgentsMd.test.d.ts +1 -0
- package/dist/src/projectConfig/recovery/parseAgentsMd.test.js +67 -0
- package/dist/src/projectConfig/recovery/resolveProjectNameFromDir.d.ts +1 -0
- package/dist/src/projectConfig/recovery/resolveProjectNameFromDir.js +11 -0
- package/dist/src/projectConfig/recovery/resolveProjectNameFromDir.test.d.ts +1 -0
- package/dist/src/projectConfig/recovery/resolveProjectNameFromDir.test.js +21 -0
- package/dist/src/projectConfig/resolveExistsProjectConfig.d.ts +2 -0
- package/dist/src/projectConfig/resolveExistsProjectConfig.js +34 -0
- package/dist/src/projectConfig/updateProjectConfig.d.ts +2 -0
- package/dist/src/projectConfig/updateProjectConfig.js +20 -0
- package/dist/src/projectConfig/validators/getIsValidProjectConfig.d.ts +2 -0
- package/dist/src/projectConfig/validators/getIsValidProjectConfig.js +6 -0
- package/dist/src/projectConfig/validators/getIsValidProjectConfig.test.d.ts +1 -0
- package/dist/src/projectConfig/validators/getIsValidProjectConfig.test.js +33 -0
- package/dist/src/projectConfig/validators/validateProjectConfig.d.ts +2 -0
- package/dist/src/projectConfig/validators/validateProjectConfig.js +30 -0
- package/dist/src/projectConfig/validators/validateProjectConfig.test.d.ts +1 -0
- package/dist/src/projectConfig/validators/validateProjectConfig.test.js +215 -0
- package/dist/src/templater/executor.d.ts +2 -0
- package/dist/src/templater/executor.js +22 -0
- package/dist/src/templater/executor.test.d.ts +1 -0
- package/dist/src/{executor.test.js → templater/executor.test.js} +25 -11
- package/dist/src/templater/helpers/getIsTypescriptOnlyFile.d.ts +1 -0
- package/dist/src/templater/helpers/getIsTypescriptOnlyFile.js +10 -0
- package/dist/src/templater/helpers/getIsTypescriptOnlyFile.test.d.ts +1 -0
- package/dist/src/templater/helpers/getIsTypescriptOnlyFile.test.js +25 -0
- package/dist/src/templater/helpers/resolveTemplateExtension.d.ts +1 -0
- package/dist/src/templater/helpers/resolveTemplateExtension.js +16 -0
- package/dist/src/templater/helpers/resolveTemplateExtension.test.d.ts +1 -0
- package/dist/src/{mappers → templater/mappers}/mapProjectConfigToTemplateModel.d.ts +1 -1
- package/dist/src/{mappers → templater/mappers}/mapProjectConfigToTemplateModel.js +6 -8
- package/dist/src/templater/mappers/mapProjectConfigToTemplateModel.test.d.ts +1 -0
- package/dist/src/{mappers → templater/mappers}/mapProjectConfigToTemplateModel.test.js +29 -23
- package/dist/src/templater/planner.d.ts +2 -0
- package/dist/src/templater/planner.js +104 -0
- package/dist/src/templater/planner.test.d.ts +1 -0
- package/dist/src/templater/planner.test.js +64 -0
- package/dist/src/templates/constants.d.ts +6 -0
- package/dist/src/templates/constants.js +11 -0
- package/dist/src/templates/helpers/resolveTemplatesDir.d.ts +1 -0
- package/dist/src/templates/helpers/resolveTemplatesDir.js +16 -0
- package/dist/src/templates/templates.d.ts +10 -0
- package/dist/src/templates/templates.js +133 -0
- package/dist/src/templates/templates.test.d.ts +1 -0
- package/dist/src/templates/templates.test.js +263 -0
- package/dist/src/templates/types.d.ts +21 -0
- package/dist/src/templates/types.js +1 -0
- package/dist/src/types.d.ts +28 -9
- package/dist/src/validators/getIsValidCategory.d.ts +2 -0
- package/dist/src/validators/getIsValidCategory.js +2 -0
- package/dist/src/validators/getIsValidCategory.test.d.ts +1 -0
- package/dist/src/validators/{isValidCategory.test.js → getIsValidCategory.test.js} +3 -3
- package/dist/src/validators/getIsValidFramework.d.ts +2 -0
- package/dist/src/validators/getIsValidFramework.js +2 -0
- package/dist/src/validators/getIsValidFramework.test.d.ts +1 -0
- package/dist/src/validators/{isValidFramework.test.js → getIsValidFramework.test.js} +3 -3
- package/dist/src/validators/getIsValidLanguage.d.ts +2 -0
- package/dist/src/validators/getIsValidLanguage.js +2 -0
- package/dist/src/validators/getIsValidLanguage.test.d.ts +1 -0
- package/dist/src/validators/{isValidLanguage.test.js → getIsValidLanguage.test.js} +3 -3
- package/dist/src/validators/getIsValidPackageManager.d.ts +2 -0
- package/dist/src/validators/getIsValidPackageManager.js +4 -0
- package/dist/src/validators/getIsValidPackageManager.test.d.ts +1 -0
- package/dist/src/validators/getIsValidPackageManager.test.js +11 -0
- package/dist/src/validators/getIsValidSlot.d.ts +2 -0
- package/dist/src/validators/getIsValidSlot.js +4 -0
- package/dist/src/validators/getIsValidSlot.test.d.ts +1 -0
- package/dist/src/validators/getIsValidSlot.test.js +26 -0
- package/dist/src/validators/validateSlots.js +40 -8
- package/dist/src/validators/validateSlots.test.js +125 -34
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -4
- package/templates/v1/vite.config.ts.ejs +27 -0
- package/templates/v2/.project-config.json.ejs +1 -0
- package/templates/v2/AGENTS.md.ejs +712 -0
- package/templates/v2/README.md.ejs +31 -0
- package/templates/v2/package.json.ejs +26 -0
- package/dist/bin/cli.js +0 -221
- package/dist/src/executor.d.ts +0 -2
- package/dist/src/executor.js +0 -22
- package/dist/src/helpers/detectPackageManager.d.ts +0 -2
- package/dist/src/helpers/isDirectory.d.ts +0 -1
- package/dist/src/helpers/isDirectory.js +0 -9
- package/dist/src/helpers/isDirectory.test.js +0 -22
- package/dist/src/helpers/isTypescriptOnlyFile.d.ts +0 -1
- package/dist/src/helpers/isTypescriptOnlyFile.js +0 -5
- package/dist/src/helpers/isTypescriptOnlyFile.test.js +0 -22
- package/dist/src/helpers/normalizeRelPath.d.ts +0 -1
- package/dist/src/helpers/normalizeRelPath.js +0 -1
- package/dist/src/helpers/normalizeRelPath.test.js +0 -19
- package/dist/src/helpers/resolveTemplateExtension.d.ts +0 -1
- package/dist/src/helpers/resolveTemplateExtension.js +0 -17
- package/dist/src/helpers/resolveTemplatesDir.d.ts +0 -1
- package/dist/src/helpers/resolveTemplatesDir.js +0 -11
- package/dist/src/helpers/resolveTemplatesDir.test.js +0 -21
- package/dist/src/planner.d.ts +0 -2
- package/dist/src/planner.js +0 -126
- package/dist/src/planner.test.js +0 -58
- package/dist/src/validators/index.d.ts +0 -6
- package/dist/src/validators/index.js +0 -6
- package/dist/src/validators/isValidCategory.d.ts +0 -2
- package/dist/src/validators/isValidCategory.js +0 -2
- package/dist/src/validators/isValidFramework.d.ts +0 -2
- package/dist/src/validators/isValidFramework.js +0 -2
- package/dist/src/validators/isValidLanguage.d.ts +0 -2
- package/dist/src/validators/isValidLanguage.js +0 -2
- package/dist/src/validators/validateProjectDir.d.ts +0 -1
- package/dist/src/validators/validateProjectDir.js +0 -6
- package/dist/src/validators/validateProjectDir.test.js +0 -19
- package/dist/bin/{cli.d.ts → cli/index.d.ts} +0 -0
- package/dist/src/{executor.test.d.ts → dependencies/resolveDependencies.test.d.ts} +0 -0
- package/dist/src/helpers/{detectPackageManager.test.d.ts → error.test.d.ts} +0 -0
- package/dist/src/helpers/{isDirectory.test.d.ts → getIsDirectoryExists.test.d.ts} +0 -0
- package/dist/src/helpers/{isTypescriptOnlyFile.test.d.ts → getIsJsxFramework.test.d.ts} +0 -0
- package/dist/src/helpers/{normalizeRelPath.test.d.ts → getIsTypescript.test.d.ts} +0 -0
- package/dist/src/helpers/{resolveTemplateExtension.test.d.ts → normalizePath.test.d.ts} +0 -0
- package/dist/src/{helpers/resolveTemplatesDir.test.d.ts → migrator/migration.test.d.ts} +0 -0
- package/dist/src/{mappers/mapProjectConfigToTemplateModel.test.d.ts → migrator/migrations/migrations.test.d.ts} +0 -0
- package/dist/src/{planner.test.d.ts → migrator/planner.test.d.ts} +0 -0
- package/dist/src/{validators/isValidCategory.test.d.ts → migrator/types.js} +0 -0
- package/dist/src/{validators/isValidFramework.test.d.ts → migrator/updates/updates.test.d.ts} +0 -0
- package/dist/src/{validators/isValidLanguage.test.d.ts → migrator/validators/getIsValidMigration.test.d.ts} +0 -0
- package/dist/src/{validators/validateProjectDir.test.d.ts → projectConfig/detectPackageManagerRuntime.test.d.ts} +0 -0
- package/dist/src/{helpers → templater/helpers}/resolveTemplateExtension.test.js +0 -0
- package/templates/{.gitignore.ejs → v1/.gitignore.ejs} +0 -0
- package/templates/{AGENTS.md.ejs → v1/AGENTS.md.ejs} +0 -0
- package/templates/{CLAUDE.md → v1/CLAUDE.md} +0 -0
- package/templates/{README.md.ejs → v1/README.md.ejs} +0 -0
- package/templates/{package.json.ejs → v1/package.json.ejs} +0 -0
- package/templates/{public → v1/public}/icon.svg +0 -0
- package/templates/{public → v1/public}/manifest.json.ejs +0 -0
- package/templates/{public → v1/public}/resolveSlotPage.js.ejs +0 -0
- package/templates/{src → v1/src}/slots/background/app.ts.ejs +0 -0
- package/templates/{src → v1/src}/slots/background/background.html.ejs +0 -0
- package/templates/{src → v1/src}/slots/background/main.ts.ejs +0 -0
- package/templates/{src/slots/slot/slot.html.ejs → v1/src/slots/slot/__SLOT_KEY__.html.ejs} +0 -0
- package/templates/{src → v1/src}/slots/slot/app.ts.ejs +0 -0
- package/templates/{src → v1/src}/slots/slot/app.tsx.ejs +0 -0
- package/templates/{src → v1/src}/slots/slot/main.css.ejs +0 -0
- package/templates/{src → v1/src}/slots/slot/main.ts.ejs +0 -0
- package/templates/{src → v1/src}/slots/slot/main.tsx.ejs +0 -0
- package/templates/{tsconfig.json.ejs → v1/tsconfig.json.ejs} +0 -0
- package/templates/{vite.config.ts.ejs → v2/vite.config.ts.ejs} +1 -1
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
<!-- AUTO-GENERATED SECTION START -->
|
|
2
|
+
# <%= name %>
|
|
3
|
+
Use this file as the primary coding contract for this starter. Use the Stripchat docs to get more details.
|
|
4
|
+
- Category: <%= category %>
|
|
5
|
+
- Slots: <%= slotsEntries.map(({ slot }) => slot).join(', ') %>
|
|
6
|
+
- Framework: <%= framework %>
|
|
7
|
+
- Language: <%= language %>
|
|
8
|
+
|
|
9
|
+
## Starter Map
|
|
10
|
+
- public/manifest.json declares slots, page keys, optional settings entry, and backend actions.
|
|
11
|
+
- public/resolveSlotPage.js is the runtime resolver. It returns a page key from views.pages or null.
|
|
12
|
+
- Root HTML files like <%= slotsEntries[0].slotCamelCased %>.html and background.html are emitted into the final archive root.
|
|
13
|
+
- Visual slot entry modules live in src/slots/<slot>/main.<%= isJsxFramework ? jsxExt : ext %>.
|
|
14
|
+
- Visual slot page code lives in src/slots/<slot>/app.<%= isJsxFramework ? jsxExt : ext %>.
|
|
15
|
+
- Background entry module lives in src/slots/background/main.<%= ext %>.
|
|
16
|
+
- Background page code lives in src/slots/background/app.<%= ext %>.
|
|
17
|
+
- If you add settings.html, it is also a root HTML file and must have its own entry module.
|
|
18
|
+
- vite.config.<%= isTypescript ? 'ts' : 'js' %> must include every root HTML file in build.rollupOptions.input.
|
|
19
|
+
|
|
20
|
+
## Template-managed Files
|
|
21
|
+
These files are generated and managed by the CLI tooling and template generation scripts. Changes may be overwritten by
|
|
22
|
+
future template updates or may cause update conflicts.
|
|
23
|
+
|
|
24
|
+
- `.project-config.json` — main project configuration.
|
|
25
|
+
- `AGENTS.md` — the section between `<!-- AUTO-GENERATED SECTION START -->` and `<!-- AUTO-GENERATED SECTION END -->`.
|
|
26
|
+
|
|
27
|
+
These files can be changed, but large structural updates may break future template updates via CLI:
|
|
28
|
+
- `vite.config.ts` — build project configuration.
|
|
29
|
+
- `package.json` — project scripts and dependencies.
|
|
30
|
+
|
|
31
|
+
This file can be overwritten safely:
|
|
32
|
+
- `README.md` — your project readme information.
|
|
33
|
+
|
|
34
|
+
## Template CLI Commands
|
|
35
|
+
Available in `package.json` for updating and managing the extension template.
|
|
36
|
+
|
|
37
|
+
Updating the template keeps your project aligned with the latest starter version, including:
|
|
38
|
+
|
|
39
|
+
- SDK updates
|
|
40
|
+
- Documentation updates
|
|
41
|
+
- Improvements to generated files
|
|
42
|
+
- Updates to `AGENTS.md`
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Update template-managed files and SDK dependencies
|
|
46
|
+
npm run template:update
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Minimal views shape:
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"version": "v2.0",
|
|
53
|
+
"views": {
|
|
54
|
+
"slots": ["EXTENSION_SLOT_MAIN_GAME_FUN", "EXTENSION_SLOT_BACKGROUND"],
|
|
55
|
+
"pages": {
|
|
56
|
+
"mainGameFun": "mainGameFun.html",
|
|
57
|
+
"background": "background.html"
|
|
58
|
+
},
|
|
59
|
+
"resolveSlotPageScript": "resolveSlotPage.js"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
## Hard Rules
|
|
64
|
+
- Call createExtHelper() exactly once in the first executed module of every page before any other SDK logic.
|
|
65
|
+
- In this starter, keep createExtHelper() in each page's app.* file.
|
|
66
|
+
- If you add settings.html, call createExtHelper() there too.
|
|
67
|
+
- Keep public/resolveSlotPage.js as plain synchronous JS. Do not bundle it, make it async, or import app code into it.
|
|
68
|
+
- public/resolveSlotPage.js must return only a key from public/manifest.json views.pages or null.
|
|
69
|
+
- settings.html does not go through public/resolveSlotPage.js.
|
|
70
|
+
- Every views.pages key must map to a real root HTML file.
|
|
71
|
+
- Every root HTML file must be included in vite.config.<%= isTypescript ? 'ts' : 'js' %> build.rollupOptions.input.
|
|
72
|
+
- Non-background slots can unmount at any time. Do not keep durable state, timers, polling, or coordination logic in them.
|
|
73
|
+
- Keep durable state, timers, polling, and cross-slot coordination in background.
|
|
74
|
+
- If you subscribe to SDK events, DOM events, or timers, clean them up on unmount or teardown.
|
|
75
|
+
- Use named handler functions when subscribing so you can unsubscribe the same reference.
|
|
76
|
+
- Slots can unmount, so you should always unsubscribe from sdk events and browser apis correctly.
|
|
77
|
+
- Use v1.ext.whisper.local for communication between this extension's frames for the current user.
|
|
78
|
+
- Use v1.ext.whisper only when every loaded client in the room should receive the message.
|
|
79
|
+
- When a user sends v1.ext.whisper during a public show, paymentData is required. Obtain it from v1.payment.tokens.spend.succeeded. The model can whisper without paymentData.
|
|
80
|
+
- Broadcast whispers arrive as v1.ext.whispered
|
|
81
|
+
- Local whispers arrive as v1.ext.whispered.local
|
|
82
|
+
- Always include a stable type field in whisper payloads.
|
|
83
|
+
- Do not send sensitive data in whisper payloads, including usernames or user IDs.
|
|
84
|
+
- Use v1.payment.tokens.spend.succeeded to confirm a spend in the paying client's iframe.
|
|
85
|
+
- Use v1.tokens.spent in the model/background iframe to react to the room-side tip.
|
|
86
|
+
- Always pass isAnonymous to v1.chat.message.send.
|
|
87
|
+
- Preserve anonymity in chat messages: use isAnonymous from v1.payment.tokens.spend.succeeded or tipData.isAnonymous, and never infer, override, or de-anonymize users.
|
|
88
|
+
- If isAnonymous is true, do not expose username, user ID, or sender identity in chat text, whispers, monitoring data, or backend action params.
|
|
89
|
+
- Do not add Save or Cancel buttons inside settings.html. The platform renders them outside the iframe.
|
|
90
|
+
- Do not use viewport @media queries. The iframe viewport is the slot container, not the browser window.
|
|
91
|
+
- Do not manage iframe scrolling with overflow: auto or overflow: scroll on html or body.
|
|
92
|
+
- Set background styles on <html>, not <body>.
|
|
93
|
+
- Use CSS variables with html[data-theme='dark'] and html[data-theme='light'] selectors for theming.
|
|
94
|
+
- The SDK sets html[data-theme] when createExtHelper() runs.
|
|
95
|
+
- Do not access the parent DOM.
|
|
96
|
+
- Do not rely on host cookies.
|
|
97
|
+
- Actions are strictly reviewed before publishing. Use it only when necessary (e.g. third-party API calls with secrets).
|
|
98
|
+
- If you use EXTENSION_SLOT_MOVEABLE_OVERLAY, define moveableSlot.width and moveableSlot.height in the manifest. Max size is 450x740.
|
|
99
|
+
- Do not invent new slot names or slot type constants. Use capitalized SNAKE_CASE in view.slots in manifest.
|
|
100
|
+
- Do not invent SDK request names, event names, method names, manifest field names, or payload keys. Use only documented names from the Stripchat docs and this starter.
|
|
101
|
+
- Prefer extending the generated wiring over replacing it.
|
|
102
|
+
- In TypeScript, treat settings and ad-hoc payloads as unknown until narrowed.
|
|
103
|
+
- Keep public/manifest.json valid JSON with no trailing commas.
|
|
104
|
+
- In TypeScript, import types from @stripchatdev/ext-helper and use them
|
|
105
|
+
- ALWAYS USE background slot to handle long-running orchestration logic — timers, polling, state machines, centralized state management, events, whispers
|
|
106
|
+
- The flow should be: visual slot -> whisper.local to background -> background handles logic. Background should subscribe to events and coordinate it
|
|
107
|
+
- Create background slot for each actor (model, viewer). Do not put model-only and viewer-only logic in the same background.
|
|
108
|
+
- Chatbot extensions have no UI — only a background slot handles all logic (receiving messages and sending chatbot messages).
|
|
109
|
+
- Do not create visual slots, HTML pages, or resolver entries for chatbot extensions.
|
|
110
|
+
- Use v1.chatbot.message.send to send messages as bot or model from background. Messages are delivered locally to each user.
|
|
111
|
+
- Use v1.chat.message.received to listen for incoming chat messages in background.
|
|
112
|
+
- The message param in v1.chatbot.message.send supports a {username} placeholder, replaced with the receiving user's name (empty string for anonymous, "Guest" for guests).
|
|
113
|
+
- Do not call v1.chatbot.message.send unconditionally inside v1.chat.message.received handlers for every message — chat rooms can have hundreds of messages per second. Use event-driven triggers (context changes, timers, tips) instead or add strict filters on message content or sender properties.
|
|
114
|
+
- Broadcast Tools are rendered only for the model of the current room.
|
|
115
|
+
- For Broadcast Tools, do not add explicit `context.user?.id === context.model?.id` guards in `public/resolveSlotPage.js` — the platform enforces model-only visibility automatically.
|
|
116
|
+
- If you use `EXTENSION_SLOT_MOVEABLE_OVERLAY`, define `moveableSlot.width` and `moveableSlot.height` in the manifest (max 450x740). Only the user controls expand/collapse; update status/content via SDK, not host UI takeover.
|
|
117
|
+
- videoDecorativeOverlay is not interactive. Do not put buttons or interactive elements in it. Use it only for animations, visualizations etc. after calling `v1.ext.activity.request`.
|
|
118
|
+
- When using `v1.ext.activity.request`, ALWAYS make sure that the actual animation is shorter then the activity duration in order to avoid immidiate slot closing after the animation ends.
|
|
119
|
+
|
|
120
|
+
## Slot Guardrails
|
|
121
|
+
- Generated slot sets for this starter:
|
|
122
|
+
- Games & Fun: EXTENSION_SLOT_MAIN_GAME_FUN, EXTENSION_SLOT_BACKGROUND, either one of EXTENSION_SLOT_RIGHT_OVERLAY or EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY
|
|
123
|
+
- Sex Toys: EXTENSION_SLOT_MAIN_SEX_TOY, EXTENSION_SLOT_BACKGROUND, EXTENSION_SLOT_MOVEABLE_OVERLAY, optional EXTENSION_SLOT_RIGHT_OVERLAY and EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY
|
|
124
|
+
- Chat Bots: EXTENSION_SLOT_BACKGROUND (no visual slots), optional EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY
|
|
125
|
+
- Broadcast Tools: EXTENSION_SLOT_BACKGROUND, optional EXTENSION_SLOT_MOVEABLE_OVERLAY
|
|
126
|
+
- Resolver slotType constants:
|
|
127
|
+
- mainGameFun -> EXTENSION_SLOT_MAIN_GAME_FUN
|
|
128
|
+
- mainSexToy -> EXTENSION_SLOT_MAIN_SEX_TOY
|
|
129
|
+
- rightOverlay -> EXTENSION_SLOT_RIGHT_OVERLAY
|
|
130
|
+
- moveableOverlay -> EXTENSION_SLOT_MOVEABLE_OVERLAY
|
|
131
|
+
- background -> EXTENSION_SLOT_BACKGROUND
|
|
132
|
+
|
|
133
|
+
## Default Runtime Pattern
|
|
134
|
+
- background owns long-lived state and coordination.
|
|
135
|
+
- Visual slots render UI from current state and forward user actions.
|
|
136
|
+
- Load initial context once with v1.ext.context.get.
|
|
137
|
+
- Keep context fresh with v1.ext.context.updated. Payload is { context: TV1ExtContext }.
|
|
138
|
+
- Send state updates to every frame of the current user with v1.ext.whisper.local.
|
|
139
|
+
- If all loaded clients need the event, use v1.ext.whisper.
|
|
140
|
+
- Assume every visual slot can mount, unmount, and remount without notice.
|
|
141
|
+
|
|
142
|
+
## Resolver
|
|
143
|
+
- Start from the generated resolver and extend it only when needed.
|
|
144
|
+
- Keep resolver logic cheap, synchronous, and defensive.
|
|
145
|
+
- context.user may be missing when the visitor is anonymous.
|
|
146
|
+
- extSettings may be missing or partial.
|
|
147
|
+
- Return null when a slot should not render.
|
|
148
|
+
- If you add model, viewer, or guest variants, add new page keys and branch here.
|
|
149
|
+
- Every returned string must be a key in manifest.json views.pages.
|
|
150
|
+
|
|
151
|
+
### Generated resolver for this project:
|
|
152
|
+
```
|
|
153
|
+
export default function resolveSlotPage(slotType, context, extSettings) {
|
|
154
|
+
switch (slotType) {
|
|
155
|
+
<% slotsEntries.forEach(({ slotCamelCased, slot }) => { -%>
|
|
156
|
+
case '<%= slot %>':
|
|
157
|
+
return '<%= slotCamelCased %>';
|
|
158
|
+
<% }); -%>
|
|
159
|
+
default:
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Example of context-aware branching:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
export default function resolveSlotPage(slotType, context, extSettings) {
|
|
169
|
+
const isBroadcaster = context.user?.id === context.model?.id;
|
|
170
|
+
const isAnotherModel = context.user?.isModel && !isBroadcaster;
|
|
171
|
+
if (slotType === 'EXTENSION_SLOT_MAIN_GAME_FUN') {
|
|
172
|
+
if (isBroadcaster) return 'mainGameFunModel';
|
|
173
|
+
if (isAnotherModel) return 'mainGameFunVisitingModel'
|
|
174
|
+
if (context.user?.isGuest) return 'mainGameFunGuest';
|
|
175
|
+
return 'mainGameFunViewer';
|
|
176
|
+
}
|
|
177
|
+
if (slotType === 'EXTENSION_SLOT_BACKGROUND') {
|
|
178
|
+
return 'background';
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
## Runtime Context
|
|
186
|
+
- context.model is the broadcaster for the current room.
|
|
187
|
+
- context.user is the current user running the extension.
|
|
188
|
+
- If context.user?.isGuest is true, the current user is a guest (TV1ExtGuestUser). The only field available is isGuest. Fields like id, username, status, hasTokens do not exist on guests.
|
|
189
|
+
- If context.user?.id === context.model?.id, the current user is the broadcaster of the current room.
|
|
190
|
+
- If context.user?.isModel is true, the current logged-in user has a model account. This can also be true for another model visiting the broadcast.
|
|
191
|
+
- Common model statuses: public, private, exclusivePrivate, groupShow, ticketShow, idle, off.
|
|
192
|
+
- Common user statuses: public, private, exclusivePrivate, groupShow, ticketShow, spy, off.
|
|
193
|
+
- High-value branching fields:
|
|
194
|
+
- context.model?.id
|
|
195
|
+
- context.model?.status
|
|
196
|
+
- context.user?.id
|
|
197
|
+
- context.user?.isModel
|
|
198
|
+
- Use `context.user?.id === context.model?.id` to determine that current user is a broadcasting model
|
|
199
|
+
- Use `context.user?.isModel` && !isBroadcaster to determine that visiting user is a model
|
|
200
|
+
- context.user?.status
|
|
201
|
+
- context.user?.hasTokens
|
|
202
|
+
|
|
203
|
+
## Entity Shapes
|
|
204
|
+
|
|
205
|
+
### TV1ExtContext
|
|
206
|
+
- model?: TV1ExtModel
|
|
207
|
+
- user?: TV1ExtUser
|
|
208
|
+
|
|
209
|
+
### TV1ExtModel
|
|
210
|
+
- id: number
|
|
211
|
+
- username: string
|
|
212
|
+
- name: string
|
|
213
|
+
- avatarUrlThumb: string
|
|
214
|
+
- gender: 'male' | 'female' | 'group' | 'tranny'
|
|
215
|
+
- status: 'public' | 'private' | 'exclusivePrivate' | 'groupShow' | 'ticketShow' | 'idle' | 'off'
|
|
216
|
+
- isPrivateEnabled: boolean
|
|
217
|
+
- isSpyEnabled: boolean
|
|
218
|
+
- isExclusivePrivateEnabled: boolean
|
|
219
|
+
- isNonNude: boolean
|
|
220
|
+
|
|
221
|
+
### TV1ExtUser
|
|
222
|
+
Discriminated union on `isGuest`: TV1ExtGuestUser | TV1ExtLoggedInUser.
|
|
223
|
+
|
|
224
|
+
TV1ExtGuestUser:
|
|
225
|
+
- isGuest: true
|
|
226
|
+
- guestHash: string;
|
|
227
|
+
|
|
228
|
+
TV1ExtLoggedInUser:
|
|
229
|
+
- isGuest: false
|
|
230
|
+
- id: number
|
|
231
|
+
- username: string
|
|
232
|
+
- status: 'public' | 'private' | 'exclusivePrivate' | 'groupShow' | 'ticketShow' | 'spy' | 'off'
|
|
233
|
+
- hasTokens: boolean
|
|
234
|
+
- hasPaidBefore: boolean
|
|
235
|
+
- hasUltimateSubscription: boolean
|
|
236
|
+
- isModel: boolean
|
|
237
|
+
- `isModel` means the logged-in user has a model account. It does not mean they are the broadcaster of the current room.
|
|
238
|
+
- userRanking?: { league: 'grey' | 'bronze' | 'silver' | 'gold' | 'diamond' | 'royal' | 'legend'; level: number }
|
|
239
|
+
|
|
240
|
+
### TV1PaymentData
|
|
241
|
+
- amount: string
|
|
242
|
+
- paymentToken: string
|
|
243
|
+
- transactionId: string
|
|
244
|
+
- userId: string
|
|
245
|
+
|
|
246
|
+
### TV1TipData (payload of v1.tokens.spent)
|
|
247
|
+
- amount: number
|
|
248
|
+
- isAnonymous: boolean
|
|
249
|
+
- isFromTheCurrentUser: boolean
|
|
250
|
+
- isOriginalSource: boolean
|
|
251
|
+
- message: string | undefined
|
|
252
|
+
- source: 'console' | 'tipMenu' | 'goal' | 'epicGoal' | 'fullscreen' | 'interactiveToy' | 'publicChat' | 'privateChat' | 'extension' | 'sendTipButton' | 'feed'
|
|
253
|
+
- type: 'private' | 'public'
|
|
254
|
+
- user: TV1ExtUser | null | undefined
|
|
255
|
+
|
|
256
|
+
### TV1GoalData
|
|
257
|
+
- description: string
|
|
258
|
+
- isCompleted: boolean
|
|
259
|
+
- tokensTarget: number
|
|
260
|
+
- tokensCollected: number
|
|
261
|
+
|
|
262
|
+
## Common SDK Calls
|
|
263
|
+
- v1.ext.context.get -> load initial room context.
|
|
264
|
+
- v1.ext.context.updated -> react to context changes. Event payload is { context: TV1ExtContext }.
|
|
265
|
+
- v1.ext.whisper.local -> send a message to every frame of the current user.
|
|
266
|
+
- v1.ext.whisper -> broadcast to all loaded clients in the room. Users in public shows must include paymentData from v1.payment.tokens.spend.succeeded.
|
|
267
|
+
- v1.ext.whispered -> receives whispers from v1.ext.whisper.
|
|
268
|
+
- v1.ext.whispered.local -> receives whisper.local messages.
|
|
269
|
+
- v1.chat.message.send -> send a chat message on behalf of the extension. The message is sent locally to each user. Always pass isAnonymous from the payment/tip source and preserve the user's anonymity. It can include a {username} placeholder. You can highlight part of the text by using {#accent}Accent text{/accent} and {#fade}Faded text{/fade}. ALWAYS use it to notify a user about game results, e.g. after v1.ext.activity.request timeout.
|
|
270
|
+
- v1.chatbot.message.send -> send a chat message as bot or model. Message is delivered locally to each user. Supports {username} placeholder in message text. Params: from ('bot' | 'model'), message (string). Use it only in chatbot extensions.
|
|
271
|
+
- v1.chat.message.received -> triggered on every new chat message. Payload: messageText (string), sender (TV1ExtUser), type ('private' | 'public').
|
|
272
|
+
- v1.ext.settings.get -> read saved settings in slot pages and background.
|
|
273
|
+
- v1.model.ext.settings.get -> load saved settings inside settings.html.
|
|
274
|
+
- v1.model.ext.settings.set.requested -> platform save event for settings.html.
|
|
275
|
+
- v1.model.ext.settings.set -> persist settings from settings.html.
|
|
276
|
+
- v1.ext.actions.call -> call backend actions declared in the manifest.
|
|
277
|
+
- v1.payment.tokens.spend -> request token spend from the current user. Result is null; confirmation arrives via event.
|
|
278
|
+
- v1.payment.tokens.spend.succeeded -> confirm successful token spend on the paying client. Payload includes isAnonymous and paymentData (TV1PaymentData) needed for v1.ext.whisper in public shows. Prefer it over v1.tokens.spent for any games or interactions that require payment confirmation.
|
|
279
|
+
- v1.payment.tokens.topup -> open the token purchase modal directly for an explicit "Buy tokens" CTA. Params null, result null. It does not confirm a spend; use v1.payment.tokens.spend when you need a confirmed payment for a game or interaction.
|
|
280
|
+
- v1.tokens.spent -> receive room-side tip events. Should be used in background to react on tips (e.g. to send a message in chat).
|
|
281
|
+
- v1.private.request -> request a private show with the broadcaster.
|
|
282
|
+
- v1.ext.signup.open -> open sign-up for anonymous users.
|
|
283
|
+
- v1.ext.activity.request -> Reserves exclusive access to shared activity slots (`EXTENSION_SLOT_RIGHT_OVERLAY`, `EXTENSION_SLOT_VIDEO_DECORATIVE_OVERLAY`) for a short visual action (for example: reactions, wheel-of-fortune spin, or a brief action indicator). The call waits until the slot is available, then starts the reservation for `durationMs` (max `30000` ms) and returns `{ activityId: string }`. Reservation is ephemeral (not persisted across reboots and not restored for newly joined users); for long-lived, persistent flows use `v1.session.*`.
|
|
284
|
+
- v1.ext.movableOverlay.update -> update moveableOverlay status pill (status + message). Sex Toys and Broadcast Tools only.
|
|
285
|
+
- v1.monitoring.report.error -> report meaningful failures.
|
|
286
|
+
- v1.monitoring.report.log -> report important lifecycle events or fallback decisions.
|
|
287
|
+
- v1.storage.string.set -> store a string value by key. Optional ttlSeconds for auto-expiry.
|
|
288
|
+
- v1.storage.string.get -> read a string value by key.
|
|
289
|
+
- v1.storage.string.append -> append text to a string value. If key does not exist, behaves like set.
|
|
290
|
+
- v1.storage.int.get -> read an integer counter value by key.
|
|
291
|
+
- v1.storage.int.increment -> add delta to an integer counter. Negative delta subtracts.
|
|
292
|
+
- v1.storage.int.reset -> reset an integer counter to 0.
|
|
293
|
+
- v1.storage.mutex.lock -> acquire an exclusive lock. Optional ttlSeconds for auto-release.
|
|
294
|
+
- v1.storage.mutex.unlock -> release a previously acquired mutex.
|
|
295
|
+
- v1.storage.mutex.getState -> check whether a mutex is currently locked.
|
|
296
|
+
- v1.storage.top.get -> read a top counter: top-N users by total within a sliding window.
|
|
297
|
+
- v1.storage.top.increment -> append an increment to a top counter for a user.
|
|
298
|
+
- v1.storage.top.reset -> clear all increments for a top counter key.
|
|
299
|
+
- v1.goal.get -> load the current active regular goal. Returns `{ goal: TV1GoalData | null }`. `goal` is `null` when no regular goal is active.
|
|
300
|
+
- v1.goal.updated -> fires when any field of the active goal changes (description, isCompleted, tokensTarget, tokensCollected) or the active goal is removed. Payload: `{ goal: TV1GoalData | null }`. Use it in background to keep the cached goal in sync and broadcast updates to visual slots via v1.ext.whisper.local.
|
|
301
|
+
- v1.session.join.model -> model-only create/start of a long-term session when none exists. If a session is already active, this call does not recreate/replace it.
|
|
302
|
+
- v1.session.join.user -> user-only join after payment (`payment` param is required and is taken from `paymentData` in `v1.payment.tokens.spend.succeeded`). If no session is active, this call can create one.
|
|
303
|
+
- v1.session.get -> returns the current long-term session. Before enabling Start/Join UI, check `v1.session.get`: if `session.owner === 'other'`, keep controls blocked because another extension owns the active session. TTL is applied from whoever created the active session first (model or user).
|
|
304
|
+
- v1.session.finish.model -> clear the current long-term session from the model side.
|
|
305
|
+
- v1.session.whisper.user -> send a custom action payload within the current long-term session from the user to the model.
|
|
306
|
+
|
|
307
|
+
## Canonical Examples and Practices
|
|
308
|
+
|
|
309
|
+
### Settings save flow in settings.html
|
|
310
|
+
```
|
|
311
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
312
|
+
const ext = createExtHelper();
|
|
313
|
+
|
|
314
|
+
const DEFAULT_SETTINGS = {
|
|
315
|
+
isEnabled: true,
|
|
316
|
+
viewerMessage: 'Welcome to the game',
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const toSettings = (value) => {
|
|
320
|
+
if (!value || typeof value !== 'object') {
|
|
321
|
+
return DEFAULT_SETTINGS;
|
|
322
|
+
}
|
|
323
|
+
const input = /** @type {Record<string, unknown>} */ (value);
|
|
324
|
+
return {
|
|
325
|
+
isEnabled:
|
|
326
|
+
typeof input.isEnabled === 'boolean' ? input.isEnabled : DEFAULT_SETTINGS.isEnabled,
|
|
327
|
+
viewerMessage:
|
|
328
|
+
typeof input.viewerMessage === 'string'
|
|
329
|
+
? input.viewerMessage
|
|
330
|
+
: DEFAULT_SETTINGS.viewerMessage,
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
const init = async () => {
|
|
334
|
+
const res = await ext.makeRequest('v1.model.ext.settings.get', null);
|
|
335
|
+
const savedSettings = toSettings(res.settings);
|
|
336
|
+
// populate form fields from savedSettings here
|
|
337
|
+
const onSettingsSaveRequested = async () => {
|
|
338
|
+
// read current values from your form fields here
|
|
339
|
+
const settings = {
|
|
340
|
+
isEnabled: true,
|
|
341
|
+
viewerMessage: 'Hello',
|
|
342
|
+
};
|
|
343
|
+
const isError =
|
|
344
|
+
typeof settings.viewerMessage !== 'string' ||
|
|
345
|
+
settings.viewerMessage.trim().length === 0;
|
|
346
|
+
await ext.makeRequest('v1.model.ext.settings.set', {
|
|
347
|
+
settings,
|
|
348
|
+
isError,
|
|
349
|
+
});
|
|
350
|
+
};
|
|
351
|
+
ext.subscribe('v1.model.ext.settings.set.requested', onSettingsSaveRequested);
|
|
352
|
+
const cleanup = () => {
|
|
353
|
+
ext.unsubscribe('v1.model.ext.settings.set.requested', onSettingsSaveRequested);
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
void init();
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Backend action manifest and call
|
|
360
|
+
Manifest snippet:
|
|
361
|
+
```
|
|
362
|
+
{
|
|
363
|
+
"actions": [
|
|
364
|
+
{
|
|
365
|
+
"name": "createToken",
|
|
366
|
+
"type": "externalCall",
|
|
367
|
+
"config": {
|
|
368
|
+
"action": "https://example.com/api/token",
|
|
369
|
+
"method": "POST"
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Call from extension code
|
|
377
|
+
```
|
|
378
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
379
|
+
const ext = createExtHelper();
|
|
380
|
+
const init = async () => {
|
|
381
|
+
const result = await ext.makeRequest('v1.ext.actions.call', {
|
|
382
|
+
actionName: 'createToken',
|
|
383
|
+
params: {},
|
|
384
|
+
});
|
|
385
|
+
if (result.code !== 200) {
|
|
386
|
+
await ext.makeRequest('v1.monitoring.report.error', {
|
|
387
|
+
message: 'createToken action failed',
|
|
388
|
+
data: { code: result.code, body: result.body },
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (!result.body || typeof result.body !== 'object') {
|
|
393
|
+
await ext.makeRequest('v1.monitoring.report.error', {
|
|
394
|
+
message: 'createToken returned invalid body',
|
|
395
|
+
data: { body: result.body },
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const body = /** @type {Record<string, unknown>} */ (result.body);
|
|
400
|
+
if (typeof body.token !== 'string') {
|
|
401
|
+
await ext.makeRequest('v1.monitoring.report.error', {
|
|
402
|
+
message: 'createToken returned invalid token',
|
|
403
|
+
data: { body: result.body },
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const token = body.token;
|
|
408
|
+
console.log('Received token:', token);
|
|
409
|
+
};
|
|
410
|
+
void init();
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Token spend in user iframe, model background reacts via v1.tokens.spent
|
|
414
|
+
User EXTENSION_SLOT_MAIN_GAME_FUN slot:
|
|
415
|
+
```
|
|
416
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
417
|
+
const ext = createExtHelper();
|
|
418
|
+
const spendForRound = async () => {
|
|
419
|
+
await ext.makeRequest('v1.payment.tokens.spend', {
|
|
420
|
+
tokensAmount: 10,
|
|
421
|
+
tokensSpendData: {
|
|
422
|
+
type: 'ROUND_JOIN',
|
|
423
|
+
roundId: 'round-1',
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
};
|
|
427
|
+
const onTokensSpendSucceeded = (data) => {
|
|
428
|
+
console.log('Spend confirmed:', data.paymentData.amount, data.tokensSpendData);
|
|
429
|
+
};
|
|
430
|
+
ext.subscribe('v1.payment.tokens.spend.succeeded', onTokensSpendSucceeded);
|
|
431
|
+
const cleanup = () => {
|
|
432
|
+
ext.unsubscribe('v1.payment.tokens.spend.succeeded', onTokensSpendSucceeded);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
void spendForRound();
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Model background slot (reacts to tip with an API call):
|
|
439
|
+
```
|
|
440
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
441
|
+
const ext = createExtHelper();
|
|
442
|
+
const onTokensSpent = async ({ tipData }) => {
|
|
443
|
+
if (!tipData.isOriginalSource) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
const result = await ext.makeRequest('v1.ext.actions.call', {
|
|
447
|
+
actionName: 'recordPayment',
|
|
448
|
+
params: {
|
|
449
|
+
amount: tipData.amount,
|
|
450
|
+
userId: tipData.user?.id,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
if (result.code !== 200) {
|
|
454
|
+
await ext.makeRequest('v1.monitoring.report.error', {
|
|
455
|
+
message: 'recordPayment action failed',
|
|
456
|
+
data: { code: result.code, body: result.body },
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
ext.subscribe('v1.tokens.spent', onTokensSpent);
|
|
461
|
+
const cleanup = () => {
|
|
462
|
+
ext.unsubscribe('v1.tokens.spent', onTokensSpent);
|
|
463
|
+
};
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### User-initiated whisper with payment token (full flow)
|
|
467
|
+
When a user needs to broadcast a whisper during a public show, paymentData is required.
|
|
468
|
+
Flow: visual slot calls v1.payment.tokens.spend -> user background listens to v1.payment.tokens.spend.succeeded -> user background sends v1.ext.whisper with paymentData.
|
|
469
|
+
|
|
470
|
+
User visual slot (initiates spend on user action):
|
|
471
|
+
```
|
|
472
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
473
|
+
const ext = createExtHelper();
|
|
474
|
+
|
|
475
|
+
const spinWheel = async () => {
|
|
476
|
+
await ext.makeRequest('v1.payment.tokens.spend', {
|
|
477
|
+
tokensAmount: 10,
|
|
478
|
+
tokensSpendData: { action: 'spin_wheel' },
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
User background slot (listens for payment confirmation, whispers with paymentData):
|
|
484
|
+
```
|
|
485
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
486
|
+
const ext = createExtHelper();
|
|
487
|
+
|
|
488
|
+
const onSpendSucceeded = async (data) => {
|
|
489
|
+
await ext.makeRequest('v1.ext.whisper', {
|
|
490
|
+
data: { type: 'SPIN_RESULT', value: 7, isAnonymous: data.isAnonymous },
|
|
491
|
+
paymentData: data.paymentData,
|
|
492
|
+
});
|
|
493
|
+
};
|
|
494
|
+
ext.subscribe('v1.payment.tokens.spend.succeeded', onSpendSucceeded);
|
|
495
|
+
const cleanup = () => {
|
|
496
|
+
ext.unsubscribe('v1.payment.tokens.spend.succeeded', onSpendSucceeded);
|
|
497
|
+
};
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
All clients receiving the broadcast:
|
|
501
|
+
```
|
|
502
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
503
|
+
const ext = createExtHelper();
|
|
504
|
+
const onWhispered = (data) => {
|
|
505
|
+
if (data.type !== 'SPIN_RESULT') {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
showResult(data.value);
|
|
509
|
+
};
|
|
510
|
+
ext.subscribe('v1.ext.whispered', onWhispered);
|
|
511
|
+
const cleanup = () => {
|
|
512
|
+
ext.unsubscribe('v1.ext.whispered', onWhispered);
|
|
513
|
+
};
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Activity request flow
|
|
517
|
+
In rightOverlay or videoDecorativeOverlay (after user initiated whisper with payment token):
|
|
518
|
+
|
|
519
|
+
```
|
|
520
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
521
|
+
const ext = createExtHelper();
|
|
522
|
+
|
|
523
|
+
const ANIMATION_DURATION = 8_000;
|
|
524
|
+
const ACTIVITY_DURATION = 10_000;
|
|
525
|
+
|
|
526
|
+
const handleWhispered = async (data) => {
|
|
527
|
+
if ('type' in data && data.type !== 'SPIN_THE_WHEEL') {
|
|
528
|
+
return;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
await ext.makeRequest('v1.ext.activity.request', {
|
|
532
|
+
durationMs: ACTIVITY_DURATION,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// do some work here, for example show an animation in rightOverlay or videoDecorativeOverlay for {ANIMATION_DURATION}. Make sure that animation duration < activity duration.
|
|
536
|
+
|
|
537
|
+
const isAnonymous = data.isAnonymous === true;
|
|
538
|
+
|
|
539
|
+
// after the activity ends, send a message in chat with the result
|
|
540
|
+
void ext.makeRequest('v1.chat.message.send', {
|
|
541
|
+
isAnonymous,
|
|
542
|
+
// where X is the result of the activity
|
|
543
|
+
message: isAnonymous ? 'An anonymous user won the X' : 'A viewer won the X',
|
|
544
|
+
user: null,
|
|
545
|
+
});
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
ext.subscribe('v1.ext.whispered', handleWhispered);
|
|
549
|
+
```
|
|
550
|
+
UI blocking should be handled in UI by listening to the activity changes. For example, in mainGameFun slot:
|
|
551
|
+
```
|
|
552
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
553
|
+
const ext = createExtHelper();
|
|
554
|
+
|
|
555
|
+
// sync initial state
|
|
556
|
+
const init = async () => {
|
|
557
|
+
const { status } = await ext.makeRequest('v1.ext.activity.status', null);
|
|
558
|
+
|
|
559
|
+
if (status === 'busy') {
|
|
560
|
+
// disable button
|
|
561
|
+
};
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// change the UI state on activity updates
|
|
565
|
+
ext.subscribe('v1.ext.activity.busy', () => {
|
|
566
|
+
// disable button
|
|
567
|
+
});
|
|
568
|
+
ext.subscribe('v1.ext.activity.available', () => {
|
|
569
|
+
// enable button
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
await init()
|
|
573
|
+
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### Welcome bot (chatbot background slot)
|
|
577
|
+
Loads context, subscribes to updates, and sends a welcome message after 30 seconds only to Ultimate subscribers in background slot.
|
|
578
|
+
```
|
|
579
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
580
|
+
const ext = createExtHelper();
|
|
581
|
+
|
|
582
|
+
const init = async () => {
|
|
583
|
+
const { context } = await ext.makeRequest('v1.ext.context.get', null);
|
|
584
|
+
let currentContext = context;
|
|
585
|
+
|
|
586
|
+
const onContextUpdated = (data) => {
|
|
587
|
+
currentContext = data.context;
|
|
588
|
+
};
|
|
589
|
+
ext.subscribe('v1.ext.context.updated', onContextUpdated);
|
|
590
|
+
|
|
591
|
+
const welcomeTimer = setTimeout(async () => {
|
|
592
|
+
if (!currentContext.user?.hasUltimateSubscription) {
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
await ext.makeRequest('v1.chatbot.message.send', {
|
|
596
|
+
from: 'bot',
|
|
597
|
+
message: 'Welcome to the room, {username}!',
|
|
598
|
+
});
|
|
599
|
+
}, 30_000);
|
|
600
|
+
|
|
601
|
+
const cleanup = () => {
|
|
602
|
+
clearTimeout(welcomeTimer);
|
|
603
|
+
ext.unsubscribe('v1.ext.context.updated', onContextUpdated);
|
|
604
|
+
};
|
|
605
|
+
};
|
|
606
|
+
void init();
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Buy tokens CTA (open the top-up modal)
|
|
610
|
+
Use v1.payment.tokens.topup for an explicit "Buy tokens" button. It opens the purchase modal directly and returns null — it does not confirm a spend. For paid game actions that need a confirmed payment, use v1.payment.tokens.spend with v1.payment.tokens.spend.succeeded instead.
|
|
611
|
+
|
|
612
|
+
Visual slot:
|
|
613
|
+
```
|
|
614
|
+
import { createExtHelper } from '@stripchatdev/ext-helper';
|
|
615
|
+
const ext = createExtHelper();
|
|
616
|
+
|
|
617
|
+
const onBuyTokensClick = () => {
|
|
618
|
+
// opens the purchase modal directly; no result and no confirmation event
|
|
619
|
+
void ext.makeRequest('v1.payment.tokens.topup', null);
|
|
620
|
+
};
|
|
621
|
+
// wire onBuyTokensClick to your "Buy tokens" button
|
|
622
|
+
|
|
623
|
+
const init = async () => {
|
|
624
|
+
const { context } = await ext.makeRequest('v1.ext.context.get', null);
|
|
625
|
+
// show the CTA only to logged-in users who are out of tokens
|
|
626
|
+
const showBuyCta =
|
|
627
|
+
!!context.user && !context.user.isGuest && !context.user.hasTokens;
|
|
628
|
+
// render or hide the "Buy tokens" button based on showBuyCta here
|
|
629
|
+
};
|
|
630
|
+
void init();
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
## Layout And Theme
|
|
634
|
+
- Width is controlled by the parent slot container.
|
|
635
|
+
- Build layouts with flexbox, grid, %, fr, min(), max(), and clamp().
|
|
636
|
+
- Set html { height: 100% }.
|
|
637
|
+
- Let body grow naturally with content.
|
|
638
|
+
- Put the page background on <html>.
|
|
639
|
+
- Do not rely on viewport-based responsive logic.
|
|
640
|
+
- Assume slot width can change at any time.
|
|
641
|
+
- Use html[data-theme='dark'] and html[data-theme='light'] for theme-specific tokens.
|
|
642
|
+
- Read [design guidelines](https://extensions.stripchat.com/docs/style-guide/guidelines) for recommended UI patterns and practices. Use recommended colors and fonts.
|
|
643
|
+
|
|
644
|
+
## Monitoring
|
|
645
|
+
- Use monitoring for meaningful failures and important lifecycle events.
|
|
646
|
+
- Keep monitoring messages short and stable.
|
|
647
|
+
- Put variable details in data.
|
|
648
|
+
- Do not spam monitoring for routine events.
|
|
649
|
+
|
|
650
|
+
## Common Changes
|
|
651
|
+
|
|
652
|
+
### Add a page variant
|
|
653
|
+
1. Create a new root HTML file at the project root.
|
|
654
|
+
2. Add its page key to public/manifest.json views.pages.
|
|
655
|
+
3. Update public/resolveSlotPage.js to return that page key or null.
|
|
656
|
+
4. Add the new HTML file to vite.config.<%= isTypescript ? 'ts' : 'js' %> input.
|
|
657
|
+
5. Create or reuse the page entry module and page code.
|
|
658
|
+
6. Keep manifest, resolver, HTML, and Vite input in sync.
|
|
659
|
+
### Add model, viewer, or guest variants
|
|
660
|
+
1. Keep the slot name the same.
|
|
661
|
+
2. Add separate page keys in views.pages.
|
|
662
|
+
3. Branch in public/resolveSlotPage.js using context.user, context.model, and context.user.isGuest. For custom broadcaster-only variants outside Broadcast Tools defaults, check `context.user?.id === context.model?.id`. Use `context.user?.isModel` only when behavior should differ for other models visiting your stream.
|
|
663
|
+
4. Return null when that actor should not render the slot.
|
|
664
|
+
5. Keep shared state in background.
|
|
665
|
+
### Add a settings page
|
|
666
|
+
1. Create settings.html as a root HTML file.
|
|
667
|
+
2. Create a page entry module for it and call createExtHelper() there.
|
|
668
|
+
3. Add views.settings to public/manifest.json.
|
|
669
|
+
4. Add settings.html to vite.config.<%= isTypescript ? 'ts' : 'js' %> input.
|
|
670
|
+
5. Load with v1.model.ext.settings.get.
|
|
671
|
+
6. Save in response to v1.model.ext.settings.set.requested.
|
|
672
|
+
7. Persist with v1.model.ext.settings.set.
|
|
673
|
+
### Add a backend action
|
|
674
|
+
1. Declare the action in public/manifest.json actions[].
|
|
675
|
+
2. Use externalCall for HTTP requests. Use delayedEvent only for deferred follow-up work.
|
|
676
|
+
3. Call the action with v1.ext.actions.call, passing all required params.
|
|
677
|
+
4. Handle non-success result.code explicitly.
|
|
678
|
+
5. Do not hardcode secrets in slot code.
|
|
679
|
+
### Add moveable overlay
|
|
680
|
+
1. Add EXTENSION_SLOT_MOVEABLE_OVERLAY to the slot configuration.
|
|
681
|
+
2. Add a page key and root HTML file for it.
|
|
682
|
+
3. Add moveableSlot to the manifest with width and height. Max size is 450x740.
|
|
683
|
+
4. Keep public/icon.svg in the archive root.
|
|
684
|
+
5. Add the HTML file to Vite input.
|
|
685
|
+
6. Update the resolver to return the page key only when appropriate.
|
|
686
|
+
### Final Check Before Finishing
|
|
687
|
+
- createExtHelper() is called once per page.
|
|
688
|
+
- public/resolveSlotPage.js is sync and returns only valid page keys or null.
|
|
689
|
+
- Every page key in views.pages points to a real root HTML file.
|
|
690
|
+
- Every root HTML file is included in Vite input.
|
|
691
|
+
- Subscriptions, timers, and DOM listeners are cleaned up.
|
|
692
|
+
- Durable logic lives in background.
|
|
693
|
+
- Theme styles are based on html[data-theme].
|
|
694
|
+
- Layout uses flexible CSS without viewport @media queries.
|
|
695
|
+
- manifest.json is valid JSON.
|
|
696
|
+
|
|
697
|
+
### References
|
|
698
|
+
- Overview: https://extensions.stripchat.com/docs/overview/how-extensions-work
|
|
699
|
+
- Manifest: https://extensions.stripchat.com/docs/getting-started/manifest
|
|
700
|
+
- Resolve slot page: https://extensions.stripchat.com/docs/getting-started/resolve-slot-page
|
|
701
|
+
- Slots: https://extensions.stripchat.com/docs/getting-started/slots
|
|
702
|
+
- Slots communication: https://extensions.stripchat.com/docs/getting-started/slots-communication
|
|
703
|
+
- Slots layout: https://extensions.stripchat.com/docs/getting-started/slots-layout
|
|
704
|
+
- Theming: https://extensions.stripchat.com/docs/getting-started/theming
|
|
705
|
+
- Settings: https://extensions.stripchat.com/docs/getting-started/settings
|
|
706
|
+
- Backend actions: https://extensions.stripchat.com/docs/getting-started/backend-actions
|
|
707
|
+
- Monitoring: https://extensions.stripchat.com/docs/getting-started/monitoring
|
|
708
|
+
- Requests: https://extensions.stripchat.com/docs/api/requests
|
|
709
|
+
- Events: https://extensions.stripchat.com/docs/api/events
|
|
710
|
+
- Entities: https://extensions.stripchat.com/docs/api/entities
|
|
711
|
+
- Session guide: https://extensions.stripchat.com/docs/guides/session-extension
|
|
712
|
+
<!-- AUTO-GENERATED SECTION END -->
|