@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 +65 -0
- package/package.descriptor.mjs +66 -0
- package/package.json +11 -0
- package/src/client/components/GoogleRewardedGateHost.vue +184 -0
- package/src/client/composables/useGoogleRewardedRuntime.js +8 -0
- package/src/client/index.js +7 -0
- package/src/client/providers/GoogleRewardedClientProvider.js +59 -0
- package/src/client/runtime/googleRewardedRuntime.js +458 -0
- package/src/index.js +1 -0
- package/src/server/index.js +1 -0
- package/src/shared/index.js +1 -0
- package/test/runtime.test.js +563 -0
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,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 };
|