@jskit-ai/google-rewarded-web 0.1.1

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,65 @@
1
+ # @jskit-ai/google-rewarded-web
2
+
3
+ Client runtime package for Google rewarded unlock gates.
4
+
5
+ ## What It Installs
6
+
7
+ This package installs:
8
+
9
+ - a client provider that mounts the rewarded gate host once
10
+ - a runtime token: `google-rewarded.web.runtime`
11
+ - a composable: `useGoogleRewardedRuntime()`
12
+
13
+ The gate host is mounted from the provider and does not depend on placements or page-level route hacks.
14
+
15
+ ## Runtime API
16
+
17
+ App feature code should not talk to GPT directly.
18
+
19
+ Use the runtime:
20
+
21
+ ```js
22
+ const rewarded = useGoogleRewardedRuntime();
23
+
24
+ const result = await rewarded.requireUnlock({
25
+ gateKey: "progress-logging",
26
+ workspaceSlug
27
+ });
28
+ ```
29
+
30
+ `requireUnlock()` resolves in four normal cases:
31
+
32
+ - already unlocked
33
+ - reward granted
34
+ - user closed the ad without reward
35
+ - provider unavailable / load failure
36
+
37
+ The runtime opens a fullscreen prompt first, then starts the rewarded ad flow only after the user opts in.
38
+
39
+ Day 0 is intentionally app-surface-only. The runtime does not accept a caller-provided surface override.
40
+
41
+ ## Google Integration
42
+
43
+ Day 0 is Google-only and uses GPT rewarded ads for web.
44
+
45
+ The runtime relies on the rewarded slot lifecycle around:
46
+
47
+ - `rewardedSlotReady`
48
+ - `rewardedSlotGranted`
49
+ - `rewardedSlotClosed`
50
+
51
+ `rewardedSlotGranted` is the authoritative unlock trigger. The runtime then calls the server `grant` endpoint before resolving the gate as granted.
52
+
53
+ ## Failure Behavior
54
+
55
+ If Google cannot provide a rewarded slot:
56
+
57
+ - the gate stays unresolved
58
+ - the runtime enters an error phase
59
+ - dismissing the error closes the started watch session through the server `close` endpoint
60
+
61
+ This keeps the session lifecycle explicit instead of leaving abandoned started rows behind.
62
+
63
+ ## Dependency
64
+
65
+ This package expects `@jskit-ai/google-rewarded-core` to be installed as well.
@@ -0,0 +1,66 @@
1
+ export default Object.freeze({
2
+ packageVersion: 1,
3
+ packageId: "@jskit-ai/google-rewarded-web",
4
+ version: "0.1.1",
5
+ kind: "runtime",
6
+ description: "Google rewarded client runtime with a fullscreen gate host and GPT orchestration.",
7
+ dependsOn: [
8
+ "@jskit-ai/google-rewarded-core",
9
+ "@jskit-ai/http-runtime",
10
+ "@jskit-ai/kernel",
11
+ "@jskit-ai/shell-web"
12
+ ],
13
+ capabilities: {
14
+ provides: ["google-rewarded.web"],
15
+ requires: []
16
+ },
17
+ runtime: {
18
+ server: {
19
+ providers: []
20
+ },
21
+ client: {
22
+ providers: [
23
+ {
24
+ entrypoint: "src/client/providers/GoogleRewardedClientProvider.js",
25
+ export: "GoogleRewardedClientProvider"
26
+ }
27
+ ]
28
+ }
29
+ },
30
+ metadata: {
31
+ apiSummary: {
32
+ surfaces: [
33
+ {
34
+ subpath: "./client",
35
+ summary: "Exports the Google rewarded client provider, runtime composable, and fullscreen gate host."
36
+ }
37
+ ],
38
+ containerTokens: {
39
+ server: [],
40
+ client: [
41
+ "google-rewarded.web.runtime"
42
+ ]
43
+ }
44
+ }
45
+ },
46
+ mutations: {
47
+ dependencies: {
48
+ runtime: {
49
+ "@jskit-ai/google-rewarded-core": "0.1.1",
50
+ "@jskit-ai/http-runtime": "0.1.63",
51
+ "@jskit-ai/kernel": "0.1.64",
52
+ "@jskit-ai/shell-web": "0.1.63"
53
+ },
54
+ dev: {}
55
+ },
56
+ packageJson: {
57
+ scripts: {}
58
+ },
59
+ procfile: {},
60
+ vite: {
61
+ proxy: []
62
+ },
63
+ text: [],
64
+ files: []
65
+ }
66
+ });
package/package.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@jskit-ai/google-rewarded-web",
3
+ "version": "0.1.1",
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
+ }
@@ -0,0 +1,184 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { useGoogleRewardedRuntime } from "../composables/useGoogleRewardedRuntime.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 || useGoogleRewardedRuntime());
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="google-rewarded-gate">
41
+ <div class="google-rewarded-gate__scrim" />
42
+ <div class="google-rewarded-gate__panel">
43
+ <div class="google-rewarded-gate__eyebrow">Rewarded unlock</div>
44
+ <h2 class="google-rewarded-gate__title">{{ titleText }}</h2>
45
+ <p class="google-rewarded-gate__description">{{ descriptionText }}</p>
46
+
47
+ <p
48
+ v-if="state?.phase === 'loading'"
49
+ class="google-rewarded-gate__status"
50
+ >
51
+ Loading Google rewarded ad…
52
+ </p>
53
+
54
+ <p
55
+ v-else-if="state?.phase === 'error'"
56
+ class="google-rewarded-gate__status google-rewarded-gate__status--error"
57
+ >
58
+ {{ state?.errorMessage || "Unable to start the rewarded ad." }}
59
+ </p>
60
+
61
+ <div class="google-rewarded-gate__actions">
62
+ <button
63
+ v-if="state?.phase === 'prompt'"
64
+ type="button"
65
+ class="google-rewarded-gate__button google-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="google-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="google-rewarded-gate__button google-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
+ .google-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
+ .google-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
+ .google-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
+ .google-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
+ .google-rewarded-gate__title {
131
+ margin: 0.5rem 0 0;
132
+ font-size: 1.5rem;
133
+ line-height: 1.2;
134
+ }
135
+
136
+ .google-rewarded-gate__description {
137
+ margin: 0.75rem 0 0;
138
+ line-height: 1.5;
139
+ color: #5b4636;
140
+ }
141
+
142
+ .google-rewarded-gate__status {
143
+ margin: 1rem 0 0;
144
+ font-size: 0.95rem;
145
+ color: #854d0e;
146
+ }
147
+
148
+ .google-rewarded-gate__status--error {
149
+ color: #991b1b;
150
+ }
151
+
152
+ .google-rewarded-gate__actions {
153
+ display: flex;
154
+ gap: 0.75rem;
155
+ margin-top: 1.25rem;
156
+ }
157
+
158
+ .google-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
+ .google-rewarded-gate__button--primary {
170
+ border-color: transparent;
171
+ background: #ea580c;
172
+ color: #fff;
173
+ }
174
+
175
+ @media (max-width: 640px) {
176
+ .google-rewarded-gate__actions {
177
+ flex-direction: column;
178
+ }
179
+
180
+ .google-rewarded-gate__button {
181
+ width: 100%;
182
+ }
183
+ }
184
+ </style>
@@ -0,0 +1,8 @@
1
+ import { inject } from "vue";
2
+ import { GOOGLE_REWARDED_RUNTIME_INJECTION_KEY } from "../runtime/googleRewardedRuntime.js";
3
+
4
+ function useGoogleRewardedRuntime() {
5
+ return inject(GOOGLE_REWARDED_RUNTIME_INJECTION_KEY, null);
6
+ }
7
+
8
+ export { useGoogleRewardedRuntime };
@@ -0,0 +1,7 @@
1
+ export { GoogleRewardedClientProvider } from "./providers/GoogleRewardedClientProvider.js";
2
+ export { default as GoogleRewardedGateHost } from "./components/GoogleRewardedGateHost.vue";
3
+ export {
4
+ GOOGLE_REWARDED_RUNTIME_INJECTION_KEY,
5
+ createGoogleRewardedRuntime
6
+ } from "./runtime/googleRewardedRuntime.js";
7
+ export { useGoogleRewardedRuntime } from "./composables/useGoogleRewardedRuntime.js";
@@ -0,0 +1,59 @@
1
+ import { h, render } from "vue";
2
+ import GoogleRewardedGateHost from "../components/GoogleRewardedGateHost.vue";
3
+ import {
4
+ GOOGLE_REWARDED_RUNTIME_INJECTION_KEY,
5
+ createGoogleRewardedRuntime
6
+ } from "../runtime/googleRewardedRuntime.js";
7
+
8
+ class GoogleRewardedClientProvider {
9
+ static id = "google-rewarded.web.client";
10
+
11
+ static dependsOn = ["shell.web.client"];
12
+
13
+ register(app) {
14
+ if (!app || typeof app.singleton !== "function") {
15
+ throw new Error("GoogleRewardedClientProvider requires application singleton().");
16
+ }
17
+
18
+ app.singleton("google-rewarded.web.runtime", () => createGoogleRewardedRuntime());
19
+ }
20
+
21
+ async boot(app) {
22
+ if (!app || typeof app.make !== "function" || typeof app.has !== "function") {
23
+ throw new Error("GoogleRewardedClientProvider boot requires application make()/has().");
24
+ }
25
+
26
+ if (!app.has("jskit.client.vue.app") || typeof document === "undefined") {
27
+ return;
28
+ }
29
+
30
+ const vueApp = app.make("jskit.client.vue.app");
31
+ const runtime = app.make("google-rewarded.web.runtime");
32
+
33
+ if (vueApp && typeof vueApp.provide === "function") {
34
+ vueApp.provide(GOOGLE_REWARDED_RUNTIME_INJECTION_KEY, runtime);
35
+ }
36
+
37
+ this.hostContainer = document.createElement("div");
38
+ this.hostContainer.dataset.jskitGoogleRewardedHost = "true";
39
+ document.body.appendChild(this.hostContainer);
40
+
41
+ const vnode = h(GoogleRewardedGateHost, {
42
+ runtime
43
+ });
44
+ vnode.appContext = vueApp?._context || null;
45
+ render(vnode, this.hostContainer);
46
+ }
47
+
48
+ shutdown() {
49
+ if (!this.hostContainer) {
50
+ return;
51
+ }
52
+
53
+ render(null, this.hostContainer);
54
+ this.hostContainer.remove();
55
+ this.hostContainer = null;
56
+ }
57
+ }
58
+
59
+ export { GoogleRewardedClientProvider };