@jskit-ai/rewarded-web 0.1.120

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/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @jskit-ai/rewarded-web
2
+
3
+ Client workflow and gate UI for application-owned rewards. The package requires
4
+ `client.vue` and an explicit `client.rewarded-delivery` capability. Installing it
5
+ does not load an ad SDK or select a provider.
6
+
7
+ ## Application use
8
+
9
+ ```js
10
+ const rewarded = useRewardedRuntime();
11
+ const result = await rewarded.requireUnlock({ gateKey: "bonus", workspaceSlug });
12
+ ```
13
+
14
+ The workflow opens a prompt before delivery. It handles existing unlocks, grants,
15
+ closed sessions and unavailable delivery through the app's `/rewarded/*` routes.
16
+ The delivery capability supplies `launchReward({ providerConfig, onReady,
17
+ onGranted, onClosed, onUnavailable })`, returning a promise for its lifecycle.
18
+ Provider configuration is opaque to the workflow. The application authorizes
19
+ server grants; browser callbacks alone do not prove provider viewing.
20
+
21
+ The existing gate is app-surface-only. The installed provider mounts its host
22
+ and supplies `client.rewarded`; direct callers use `createRewardedRuntime({
23
+ launchReward })`. Missing delivery fails rather than selecting Google.
24
+
25
+ ## Google delivery pattern
26
+
27
+ See [Google rewarded delivery](patterns/google-rewarded/PATTERN.md). Copy the
28
+ adapter and provider into the application and register its provider alongside
29
+ `RewardedClientProvider`. The example handles Publisher Tag script loading,
30
+ rewarded events and slot cleanup. It accepts provider `google-publisher-tag` and
31
+ uses the configured placement as the Google ad unit path.
32
+
33
+ ## V0 migration
34
+
35
+ Replace old package imports, `GoogleRewarded*` symbols and `google-rewarded.*`
36
+ capabilities with the generic names. Replace `useGoogleRewardedRuntime()` with
37
+ `useRewardedRuntime()` and supply delivery explicitly for direct construction.
38
+ There is no compatibility fallback. The core README describes table, route and
39
+ configuration migration. Install `@jskit-ai/rewarded-core` for server workflow
40
+ ownership; the Google example remains application source.
41
+
42
+ The package manifest lists application-owned inputs in both `capabilities.requires`
43
+ and `capabilities.applicationRequires`. The latter identifies who supplies them;
44
+ it does not make the runtime dependency optional or provide a default.
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@jskit-ai/rewarded-web",
3
+ "version": "0.1.120",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.js",
7
+ "./client": "./src/client/index.js",
8
+ "./server": "./src/server/index.js",
9
+ "./shared": "./src/shared/index.js"
10
+ },
11
+ "description": "Rewarded gate client workflow with explicitly composed application delivery.",
12
+ "jskit": {
13
+ "kind": "runtime",
14
+ "capabilities": {
15
+ "provides": [
16
+ "rewarded.web"
17
+ ],
18
+ "requires": [
19
+ "client.vue",
20
+ "client.rewarded-delivery"
21
+ ],
22
+ "applicationRequires": [
23
+ "client.rewarded-delivery"
24
+ ]
25
+ },
26
+ "runtime": {
27
+ "server": {
28
+ "providers": []
29
+ },
30
+ "client": {
31
+ "providers": [
32
+ {
33
+ "entrypoint": "src/client/providers/RewardedClientProvider.js",
34
+ "export": "RewardedClientProvider"
35
+ }
36
+ ]
37
+ }
38
+ },
39
+ "metadata": {
40
+ "apiSummary": {
41
+ "surfaces": [
42
+ {
43
+ "subpath": "./client",
44
+ "summary": "Exports the Rewarded client provider, runtime composable, and fullscreen gate host."
45
+ }
46
+ ]
47
+ },
48
+ "client": {
49
+ "singleton": true
50
+ }
51
+ }
52
+ },
53
+ "dependencies": {
54
+ "@jskit-ai/rewarded-core": "0.1.120"
55
+ },
56
+ "peerDependencies": {
57
+ "@jskit-ai/http-runtime": "0.1.182",
58
+ "@jskit-ai/kernel": "0.1.184",
59
+ "@jskit-ai/shell-web": "0.1.188"
60
+ }
61
+ }
@@ -0,0 +1,69 @@
1
+ ---
2
+ id: rewards/google-rewarded
3
+ title: Google rewarded delivery
4
+ summary: Compose application-owned Google Publisher Tag delivery with the generic rewarded workflow.
5
+ keywords: rewarded, google, publisher tag, ads, unlock
6
+ requires: @jskit-ai/rewarded-web
7
+ ---
8
+
9
+ # Google rewarded delivery
10
+
11
+ ## Use when
12
+
13
+ The application owner has selected Google rewarded ads and configured its
14
+ publisher account and ad unit.
15
+
16
+ ## Do not use when
17
+
18
+ The application needs a Google Ads API connection, application login, or an
19
+ independent server proof of ad viewing.
20
+
21
+ ## Product decisions
22
+
23
+ Choose the rewarded feature, user eligibility, publisher account, placement,
24
+ consent experience and server grant policy. The app owns these decisions.
25
+
26
+ ## Invariants
27
+
28
+ - Generic workflow code does not choose a delivery provider.
29
+ - The app explicitly provides `client.rewarded-delivery`.
30
+ - Browser reward events are not independent server verification of viewing.
31
+ - The adapter removes listeners and slots when settled.
32
+
33
+ ## Framework APIs
34
+
35
+ Register `GoogleRewardedDeliveryProvider` in ordinary application client
36
+ capability composition beside `RewardedClientProvider` from
37
+ `@jskit-ai/rewarded-web/client`. The example supplies
38
+ `client.rewarded-delivery`; the generic UI supplies `client.rewarded`.
39
+ Use `useRewardedRuntime().requireUnlock({ gateKey, workspaceSlug })` for gates.
40
+ Separately supply server capability `rewarded.grant-policy` with
41
+ `authorizeGrant({ session, context, trx })`; only `true` permits a grant.
42
+ This delivery example does not provide or choose the server policy.
43
+
44
+ ## Example files
45
+
46
+ Copy `example/googlePublisherTag.js` and
47
+ `example/GoogleRewardedDeliveryProvider.js` into application source. The adapter
48
+ loads Publisher Tag after the user starts the reward flow and maps
49
+ ready/granted/closed events to workflow callbacks.
50
+
51
+ ## Variation points
52
+
53
+ Configure the app's reward provider row with `provider: "google-publisher-tag"`
54
+ and `placement` equal to its ad unit path. Choose gate limits and duration through
55
+ the core workflow's rule configuration. Use a different delivery provider for
56
+ other services; do not add implicit fallback inside the generic runtime.
57
+
58
+ ## Verification
59
+
60
+ Use controlled SDK events to verify grant, close, unavailable slot and cleanup.
61
+ Test server authorization and isolation with the application's own user context.
62
+ Check duplicate grant and expired unlock behavior before enabling the feature.
63
+
64
+ ## Avoid
65
+
66
+ - claiming browser events prove provider viewing to the server
67
+ - enabling Google delivery without an explicit application choice
68
+ - adding API credentials or provider registrations to generic workflow code
69
+ - treating an Ads API connector as rewarded browser delivery
@@ -0,0 +1,12 @@
1
+ import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
2
+ import { launchGoogleRewardedAd } from "./googlePublisherTag.js";
3
+
4
+ const GoogleRewardedDeliveryProvider = defineProvider({
5
+ id: "app.rewarded.google",
6
+ provides: { delivery: "client.rewarded-delivery" },
7
+ setup() {
8
+ return { delivery: { launchReward: launchGoogleRewardedAd } };
9
+ }
10
+ });
11
+
12
+ export { GoogleRewardedDeliveryProvider };
@@ -0,0 +1,142 @@
1
+ let gptLoadPromise = null;
2
+ let gptServicesEnabled = false;
3
+
4
+ async function ensureGooglePublisherTagLoaded() {
5
+ if (typeof window === "undefined" || typeof document === "undefined") {
6
+ throw new Error("Rewarded runtime requires a browser environment.");
7
+ }
8
+
9
+ if (window.googletag?.apiReady === true) {
10
+ return window.googletag;
11
+ }
12
+ if (gptLoadPromise) {
13
+ return gptLoadPromise;
14
+ }
15
+
16
+ window.googletag ||= { cmd: [] };
17
+ gptLoadPromise = new Promise((resolve, reject) => {
18
+ const existingScript = document.querySelector('script[data-jskit-rewarded-gpt="true"]');
19
+ if (existingScript) {
20
+ existingScript.addEventListener("load", () => resolve(window.googletag), { once: true });
21
+ existingScript.addEventListener("error", () => reject(new Error("Failed to load Google Publisher Tag.")), { once: true });
22
+ return;
23
+ }
24
+
25
+ const script = document.createElement("script");
26
+ script.async = true;
27
+ script.src = "https://securepubads.g.doubleclick.net/tag/js/gpt.js";
28
+ script.dataset.jskitRewardedGpt = "true";
29
+ script.addEventListener("load", () => resolve(window.googletag), { once: true });
30
+ script.addEventListener("error", () => reject(new Error("Failed to load Google Publisher Tag.")), { once: true });
31
+ document.head.appendChild(script);
32
+ }).catch((error) => {
33
+ gptLoadPromise = null;
34
+ throw error;
35
+ });
36
+
37
+ return gptLoadPromise;
38
+ }
39
+
40
+ function launchGoogleRewardedAd({
41
+ providerConfig,
42
+ onReady = null,
43
+ onGranted = null,
44
+ onClosed = null,
45
+ onUnavailable = null
46
+ } = {}) {
47
+ if (providerConfig?.provider !== "google-publisher-tag") {
48
+ throw new TypeError("Google delivery requires provider google-publisher-tag.");
49
+ }
50
+ const placement = String(providerConfig?.placement || "").trim();
51
+ if (!placement) throw new TypeError("Rewarded delivery requires placement.");
52
+ return ensureGooglePublisherTagLoaded().then((googletag) =>
53
+ new Promise((resolve, reject) => {
54
+ googletag.cmd.push(() => {
55
+ const pubads = googletag.pubads();
56
+ const rewardedFormat = googletag?.enums?.OutOfPageFormat?.REWARDED;
57
+ if (!rewardedFormat) {
58
+ reject(new Error("Rewarded ads are not supported by this GPT build."));
59
+ return;
60
+ }
61
+
62
+ const slot = googletag.defineOutOfPageSlot(placement, rewardedFormat);
63
+ if (!slot) {
64
+ Promise.resolve(onUnavailable?.()).finally(() => {
65
+ reject(new Error("Rewarded ads are not available on this page."));
66
+ });
67
+ return;
68
+ }
69
+
70
+ let readySeen = false;
71
+ let settled = false;
72
+
73
+ const cleanup = () => {
74
+ if (typeof pubads.removeEventListener === "function") {
75
+ pubads.removeEventListener("rewardedSlotReady", handleReady);
76
+ pubads.removeEventListener("rewardedSlotGranted", handleGranted);
77
+ pubads.removeEventListener("rewardedSlotClosed", handleClosed);
78
+ }
79
+ if (typeof googletag.destroySlots === "function") {
80
+ googletag.destroySlots([slot]);
81
+ }
82
+ };
83
+
84
+ const handleReady = async (event) => {
85
+ if (event?.slot !== slot || settled) {
86
+ return;
87
+ }
88
+ readySeen = true;
89
+ await Promise.resolve(onReady?.());
90
+ if (typeof event.makeRewardedVisible === "function") {
91
+ event.makeRewardedVisible();
92
+ }
93
+ };
94
+
95
+ const handleGranted = async (event) => {
96
+ if (event?.slot !== slot || settled) {
97
+ return;
98
+ }
99
+ await Promise.resolve(onGranted?.());
100
+ };
101
+
102
+ const handleClosed = async (event) => {
103
+ if (event?.slot !== slot || settled) {
104
+ return;
105
+ }
106
+ settled = true;
107
+ cleanup();
108
+ try {
109
+ await Promise.resolve(onClosed?.());
110
+ resolve();
111
+ } catch (error) {
112
+ reject(error);
113
+ }
114
+ };
115
+
116
+ pubads.addEventListener("rewardedSlotReady", handleReady);
117
+ pubads.addEventListener("rewardedSlotGranted", handleGranted);
118
+ pubads.addEventListener("rewardedSlotClosed", handleClosed);
119
+ slot.addService(pubads);
120
+
121
+ if (!gptServicesEnabled) {
122
+ googletag.enableServices();
123
+ gptServicesEnabled = true;
124
+ }
125
+
126
+ googletag.display(slot);
127
+ window.setTimeout(() => {
128
+ if (settled || readySeen) {
129
+ return;
130
+ }
131
+ settled = true;
132
+ cleanup();
133
+ Promise.resolve(onUnavailable?.()).finally(() => {
134
+ reject(new Error("No rewarded ad was available."));
135
+ });
136
+ }, 10000);
137
+ });
138
+ })
139
+ );
140
+ }
141
+
142
+ export { launchGoogleRewardedAd };
@@ -0,0 +1,184 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useRewardedRuntime } from "../composables/useRewardedRuntime.js";
4
+
5
+ const props = defineProps({
6
+ runtime: {
7
+ type: Object,
8
+ required: false,
9
+ default: null
10
+ }
11
+ });
12
+
13
+ const runtime = computed(() => props.runtime || useRewardedRuntime());
14
+ const state = computed(() => runtime.value?.state || null);
15
+ const isVisible = computed(() => {
16
+ const currentState = state.value;
17
+ return currentState?.open === true && currentState?.phase !== "showing-ad";
18
+ });
19
+ const titleText = computed(() => state.value?.gateState?.rule?.title || "Watch an ad to continue");
20
+ const descriptionText = computed(() =>
21
+ state.value?.gateState?.rule?.description ||
22
+ "Watch a rewarded ad to unlock this action."
23
+ );
24
+
25
+ function handleWatchClick() {
26
+ runtime.value?.beginWatch?.();
27
+ }
28
+
29
+ function handleCancelClick() {
30
+ runtime.value?.cancelPrompt?.();
31
+ }
32
+
33
+ function handleDismissError() {
34
+ runtime.value?.dismissError?.();
35
+ }
36
+ </script>
37
+
38
+ <template>
39
+ <Teleport to="body">
40
+ <div v-if="isVisible" class="rewarded-gate">
41
+ <div class="rewarded-gate__scrim" />
42
+ <div class="rewarded-gate__panel">
43
+ <div class="rewarded-gate__eyebrow">Rewarded unlock</div>
44
+ <h2 class="rewarded-gate__title">{{ titleText }}</h2>
45
+ <p class="rewarded-gate__description">{{ descriptionText }}</p>
46
+
47
+ <p
48
+ v-if="state?.phase === 'loading'"
49
+ class="rewarded-gate__status"
50
+ >
51
+ Loading Rewarded ad…
52
+ </p>
53
+
54
+ <p
55
+ v-else-if="state?.phase === 'error'"
56
+ class="rewarded-gate__status rewarded-gate__status--error"
57
+ >
58
+ {{ state?.errorMessage || "Unable to start the rewarded ad." }}
59
+ </p>
60
+
61
+ <div class="rewarded-gate__actions">
62
+ <button
63
+ v-if="state?.phase === 'prompt'"
64
+ type="button"
65
+ class="rewarded-gate__button rewarded-gate__button--primary"
66
+ @click="handleWatchClick"
67
+ >
68
+ Watch ad
69
+ </button>
70
+
71
+ <button
72
+ v-if="state?.phase === 'prompt'"
73
+ type="button"
74
+ class="rewarded-gate__button"
75
+ @click="handleCancelClick"
76
+ >
77
+ Not now
78
+ </button>
79
+
80
+ <button
81
+ v-if="state?.phase === 'error'"
82
+ type="button"
83
+ class="rewarded-gate__button rewarded-gate__button--primary"
84
+ @click="handleDismissError"
85
+ >
86
+ Close
87
+ </button>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </Teleport>
92
+ </template>
93
+
94
+ <style scoped>
95
+ .rewarded-gate {
96
+ position: fixed;
97
+ inset: 0;
98
+ z-index: 2147483000;
99
+ display: flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ }
103
+
104
+ .rewarded-gate__scrim {
105
+ position: absolute;
106
+ inset: 0;
107
+ background:
108
+ linear-gradient(180deg, rgba(15, 23, 42, 0.82), rgba(2, 6, 23, 0.94));
109
+ backdrop-filter: blur(4px);
110
+ }
111
+
112
+ .rewarded-gate__panel {
113
+ position: relative;
114
+ width: min(32rem, calc(100vw - 2rem));
115
+ border-radius: 1.5rem;
116
+ padding: 1.5rem;
117
+ background: #fffaf0;
118
+ color: #201510;
119
+ box-shadow: 0 2rem 5rem rgba(15, 23, 42, 0.35);
120
+ }
121
+
122
+ .rewarded-gate__eyebrow {
123
+ font-size: 0.75rem;
124
+ font-weight: 700;
125
+ letter-spacing: 0.12em;
126
+ text-transform: uppercase;
127
+ color: #9a3412;
128
+ }
129
+
130
+ .rewarded-gate__title {
131
+ margin: 0.5rem 0 0;
132
+ font-size: 1.5rem;
133
+ line-height: 1.2;
134
+ }
135
+
136
+ .rewarded-gate__description {
137
+ margin: 0.75rem 0 0;
138
+ line-height: 1.5;
139
+ color: #5b4636;
140
+ }
141
+
142
+ .rewarded-gate__status {
143
+ margin: 1rem 0 0;
144
+ font-size: 0.95rem;
145
+ color: #854d0e;
146
+ }
147
+
148
+ .rewarded-gate__status--error {
149
+ color: #991b1b;
150
+ }
151
+
152
+ .rewarded-gate__actions {
153
+ display: flex;
154
+ gap: 0.75rem;
155
+ margin-top: 1.25rem;
156
+ }
157
+
158
+ .rewarded-gate__button {
159
+ appearance: none;
160
+ border: 1px solid #d6d3d1;
161
+ border-radius: 999px;
162
+ padding: 0.8rem 1.2rem;
163
+ background: #fff;
164
+ color: inherit;
165
+ font: inherit;
166
+ cursor: pointer;
167
+ }
168
+
169
+ .rewarded-gate__button--primary {
170
+ border-color: transparent;
171
+ background: #ea580c;
172
+ color: #fff;
173
+ }
174
+
175
+ @media (max-width: 640px) {
176
+ .rewarded-gate__actions {
177
+ flex-direction: column;
178
+ }
179
+
180
+ .rewarded-gate__button {
181
+ width: 100%;
182
+ }
183
+ }
184
+ </style>
@@ -0,0 +1,8 @@
1
+ import { inject } from "vue";
2
+ import { REWARDED_RUNTIME_INJECTION_KEY } from "../runtime/rewardedRuntime.js";
3
+
4
+ function useRewardedRuntime() {
5
+ return inject(REWARDED_RUNTIME_INJECTION_KEY, null);
6
+ }
7
+
8
+ export { useRewardedRuntime };
@@ -0,0 +1,7 @@
1
+ export { RewardedClientProvider } from "./providers/RewardedClientProvider.js";
2
+ export { default as RewardedGateHost } from "./components/RewardedGateHost.vue";
3
+ export {
4
+ REWARDED_RUNTIME_INJECTION_KEY,
5
+ createRewardedRuntime
6
+ } from "./runtime/rewardedRuntime.js";
7
+ export { useRewardedRuntime } from "./composables/useRewardedRuntime.js";
@@ -0,0 +1,60 @@
1
+ import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
2
+ import { h, render } from "vue";
3
+ import RewardedGateHost from "../components/RewardedGateHost.vue";
4
+ import {
5
+ REWARDED_RUNTIME_INJECTION_KEY,
6
+ createRewardedRuntime
7
+ } from "../runtime/rewardedRuntime.js";
8
+
9
+
10
+ const mountedHosts = new WeakMap();
11
+
12
+ const RewardedClientProvider = defineProvider({
13
+ id: "rewarded.web.client",
14
+ requires: {
15
+ vueApp: "client.vue",
16
+ delivery: "client.rewarded-delivery"
17
+ },
18
+ provides: {
19
+ rewarded: "client.rewarded"
20
+ },
21
+ setup({ delivery }) {
22
+ return {
23
+ rewarded: createRewardedRuntime({ launchReward: delivery.launchReward })
24
+ };
25
+ },
26
+ async boot({ vueApp }, { outputs }) {
27
+ if (typeof document === "undefined") {
28
+ return;
29
+ }
30
+
31
+ const runtime = outputs.rewarded;
32
+
33
+ if (vueApp && typeof vueApp.provide === "function") {
34
+ vueApp.provide(REWARDED_RUNTIME_INJECTION_KEY, runtime);
35
+ }
36
+
37
+ const hostContainer = document.createElement("div");
38
+ hostContainer.dataset.jskitRewardedHost = "true";
39
+ document.body.appendChild(hostContainer);
40
+
41
+ const vnode = h(RewardedGateHost, {
42
+ runtime
43
+ });
44
+ vnode.appContext = vueApp?._context || null;
45
+ render(vnode, hostContainer);
46
+ mountedHosts.set(runtime, hostContainer);
47
+ },
48
+ shutdown(_dependencies, { outputs }) {
49
+ const hostContainer = mountedHosts.get(outputs.rewarded);
50
+ if (!hostContainer) {
51
+ return;
52
+ }
53
+
54
+ render(null, hostContainer);
55
+ hostContainer.remove();
56
+ mountedHosts.delete(outputs.rewarded);
57
+ }
58
+ });
59
+
60
+ export { RewardedClientProvider };