@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 +44 -0
- package/package.json +61 -0
- package/patterns/google-rewarded/PATTERN.md +69 -0
- package/patterns/google-rewarded/example/GoogleRewardedDeliveryProvider.js +12 -0
- package/patterns/google-rewarded/example/googlePublisherTag.js +142 -0
- package/src/client/components/RewardedGateHost.vue +184 -0
- package/src/client/composables/useRewardedRuntime.js +8 -0
- package/src/client/index.js +7 -0
- package/src/client/providers/RewardedClientProvider.js +60 -0
- package/src/client/runtime/rewardedRuntime.js +325 -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 +600 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { reactive } from "vue";
|
|
2
|
+
import { createHttpClient } from "@jskit-ai/http-runtime/client";
|
|
3
|
+
|
|
4
|
+
const REWARDED_RUNTIME_INJECTION_KEY = Symbol("rewarded.web.runtime");
|
|
5
|
+
const REWARDED_CONFIGURATION_REASONS = new Set([
|
|
6
|
+
"rule-not-configured",
|
|
7
|
+
"provider-not-configured"
|
|
8
|
+
]);
|
|
9
|
+
const REWARDED_NON_BLOCKING_REASONS = new Set([
|
|
10
|
+
"already-unlocked",
|
|
11
|
+
"cooldown-active",
|
|
12
|
+
"daily-limit-reached",
|
|
13
|
+
...REWARDED_CONFIGURATION_REASONS
|
|
14
|
+
]);
|
|
15
|
+
const rewardedHttpClient = createHttpClient({
|
|
16
|
+
credentials: "include",
|
|
17
|
+
csrf: {
|
|
18
|
+
sessionPath: "/api/session"
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function createInitialState() {
|
|
23
|
+
return {
|
|
24
|
+
open: false,
|
|
25
|
+
phase: "idle",
|
|
26
|
+
errorMessage: "",
|
|
27
|
+
gateState: null,
|
|
28
|
+
session: null,
|
|
29
|
+
request: null
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function applyState(target, source) {
|
|
34
|
+
for (const key of Object.keys(target)) {
|
|
35
|
+
if (!Object.hasOwn(source, key)) {
|
|
36
|
+
delete target[key];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const [key, value] of Object.entries(source)) {
|
|
40
|
+
target[key] = value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeWorkspaceSlug(value = "") {
|
|
45
|
+
return String(value || "").trim().toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isWellFormedGateState(gateState = null) {
|
|
49
|
+
if (!gateState ||
|
|
50
|
+
typeof gateState !== "object" ||
|
|
51
|
+
typeof gateState.enabled !== "boolean" ||
|
|
52
|
+
typeof gateState.blocked !== "boolean") {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (gateState.blocked === true) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const reason = String(gateState.reason || "").trim().toLowerCase();
|
|
61
|
+
return Boolean(gateState.unlock) || REWARDED_NON_BLOCKING_REASONS.has(reason);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildApiPath(workspaceSlug = "", action = "", query = null) {
|
|
65
|
+
const normalizedWorkspaceSlug = normalizeWorkspaceSlug(workspaceSlug);
|
|
66
|
+
const pathname = `/api/w/${encodeURIComponent(normalizedWorkspaceSlug)}/rewarded/${action}`;
|
|
67
|
+
if (!(query instanceof URLSearchParams) || [...query.keys()].length < 1) {
|
|
68
|
+
return pathname;
|
|
69
|
+
}
|
|
70
|
+
return `${pathname}?${query.toString()}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createRewardedRuntime({ launchReward } = {}) {
|
|
74
|
+
if (typeof launchReward !== "function") {
|
|
75
|
+
throw new TypeError("Rewarded runtime requires an explicit launchReward delivery function.");
|
|
76
|
+
}
|
|
77
|
+
const state = reactive(createInitialState());
|
|
78
|
+
let pendingResolve = null;
|
|
79
|
+
let activeGrantPromise = null;
|
|
80
|
+
|
|
81
|
+
function resetState() {
|
|
82
|
+
activeGrantPromise = null;
|
|
83
|
+
pendingResolve = null;
|
|
84
|
+
applyState(state, createInitialState());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function settle(result) {
|
|
88
|
+
const resolver = pendingResolve;
|
|
89
|
+
resetState();
|
|
90
|
+
if (typeof resolver === "function") {
|
|
91
|
+
resolver(result);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function requestCurrent(input = {}) {
|
|
96
|
+
const params = new URLSearchParams({
|
|
97
|
+
gateKey: String(input.gateKey || "")
|
|
98
|
+
});
|
|
99
|
+
return rewardedHttpClient.request(
|
|
100
|
+
buildApiPath(input.workspaceSlug, "current", params),
|
|
101
|
+
{
|
|
102
|
+
method: "GET"
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function requestStart(input = {}) {
|
|
108
|
+
return rewardedHttpClient.request(
|
|
109
|
+
buildApiPath(input.workspaceSlug, "start"),
|
|
110
|
+
{
|
|
111
|
+
method: "POST",
|
|
112
|
+
body: {
|
|
113
|
+
gateKey: input.gateKey
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function requestGrant(input = {}) {
|
|
120
|
+
return rewardedHttpClient.request(
|
|
121
|
+
buildApiPath(input.workspaceSlug, "grant"),
|
|
122
|
+
{
|
|
123
|
+
method: "POST",
|
|
124
|
+
body: {
|
|
125
|
+
sessionId: input.sessionId
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function requestClose(input = {}) {
|
|
132
|
+
return rewardedHttpClient.request(
|
|
133
|
+
buildApiPath(input.workspaceSlug, "close"),
|
|
134
|
+
{
|
|
135
|
+
method: "POST",
|
|
136
|
+
body: {
|
|
137
|
+
sessionId: input.sessionId
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function requireUnlock(request = {}) {
|
|
144
|
+
const gateKey = String(request?.gateKey || "").trim();
|
|
145
|
+
const workspaceSlug = normalizeWorkspaceSlug(request?.workspaceSlug);
|
|
146
|
+
|
|
147
|
+
if (!gateKey) {
|
|
148
|
+
throw new Error("requireUnlock requires gateKey.");
|
|
149
|
+
}
|
|
150
|
+
if (!workspaceSlug) {
|
|
151
|
+
throw new Error("requireUnlock requires workspaceSlug.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const gateState = await requestCurrent({
|
|
155
|
+
gateKey,
|
|
156
|
+
workspaceSlug
|
|
157
|
+
});
|
|
158
|
+
if (!isWellFormedGateState(gateState)) {
|
|
159
|
+
throw new Error("Rewarded gate returned an invalid state.");
|
|
160
|
+
}
|
|
161
|
+
const alreadyUnlocked = gateState?.blocked === false && gateState?.unlock;
|
|
162
|
+
if (!gateState?.enabled || !gateState?.blocked) {
|
|
163
|
+
return {
|
|
164
|
+
granted: Boolean(alreadyUnlocked),
|
|
165
|
+
state: gateState
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (pendingResolve) {
|
|
170
|
+
throw new Error("A Rewarded gate is already active.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
applyState(state, {
|
|
174
|
+
open: true,
|
|
175
|
+
phase: "prompt",
|
|
176
|
+
errorMessage: "",
|
|
177
|
+
gateState,
|
|
178
|
+
session: null,
|
|
179
|
+
request: {
|
|
180
|
+
gateKey,
|
|
181
|
+
workspaceSlug
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
pendingResolve = resolve;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function beginWatch() {
|
|
191
|
+
if (!state.request || state.phase !== "prompt") {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
applyState(state, {
|
|
196
|
+
...state,
|
|
197
|
+
phase: "loading"
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const startState = await requestStart(state.request);
|
|
202
|
+
if (!startState?.session || !startState?.providerConfig) {
|
|
203
|
+
settle({
|
|
204
|
+
granted: false,
|
|
205
|
+
state: startState
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
applyState(state, {
|
|
211
|
+
...state,
|
|
212
|
+
gateState: startState,
|
|
213
|
+
session: startState.session,
|
|
214
|
+
errorMessage: ""
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await launchReward({
|
|
218
|
+
providerConfig: startState.providerConfig,
|
|
219
|
+
onReady() {
|
|
220
|
+
applyState(state, {
|
|
221
|
+
...state,
|
|
222
|
+
phase: "showing-ad"
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
async onGranted() {
|
|
226
|
+
activeGrantPromise = requestGrant({
|
|
227
|
+
workspaceSlug: state.request.workspaceSlug,
|
|
228
|
+
sessionId: state.session?.id
|
|
229
|
+
});
|
|
230
|
+
const grantResult = await activeGrantPromise;
|
|
231
|
+
applyState(state, {
|
|
232
|
+
...state,
|
|
233
|
+
phase: "granted",
|
|
234
|
+
gateState: {
|
|
235
|
+
...state.gateState,
|
|
236
|
+
...grantResult,
|
|
237
|
+
blocked: false,
|
|
238
|
+
unlock: grantResult.unlock || null
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
async onClosed() {
|
|
243
|
+
if (activeGrantPromise) {
|
|
244
|
+
const grantResult = await activeGrantPromise;
|
|
245
|
+
settle({
|
|
246
|
+
granted: true,
|
|
247
|
+
state: {
|
|
248
|
+
...state.gateState,
|
|
249
|
+
...grantResult,
|
|
250
|
+
blocked: false,
|
|
251
|
+
unlock: grantResult.unlock || null
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const closeResult = await requestClose({
|
|
258
|
+
workspaceSlug: state.request.workspaceSlug,
|
|
259
|
+
sessionId: state.session?.id
|
|
260
|
+
});
|
|
261
|
+
settle({
|
|
262
|
+
granted: false,
|
|
263
|
+
state: {
|
|
264
|
+
...state.gateState,
|
|
265
|
+
...closeResult,
|
|
266
|
+
blocked: true
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
onUnavailable() {
|
|
271
|
+
applyState(state, {
|
|
272
|
+
...state,
|
|
273
|
+
phase: "error",
|
|
274
|
+
errorMessage: "No rewarded ad was available right now. Please try again later."
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
} catch (error) {
|
|
279
|
+
applyState(state, {
|
|
280
|
+
...state,
|
|
281
|
+
phase: "error",
|
|
282
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function cancelPrompt() {
|
|
288
|
+
if (!state.request) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (state.session?.id) {
|
|
293
|
+
try {
|
|
294
|
+
await requestClose({
|
|
295
|
+
workspaceSlug: state.request.workspaceSlug,
|
|
296
|
+
sessionId: state.session.id
|
|
297
|
+
});
|
|
298
|
+
} catch {
|
|
299
|
+
// Preserve the original user intent even if cleanup fails.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
settle({
|
|
304
|
+
granted: false,
|
|
305
|
+
state: state.gateState
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function dismissError() {
|
|
310
|
+
await cancelPrompt();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return Object.freeze({
|
|
314
|
+
state,
|
|
315
|
+
requireUnlock,
|
|
316
|
+
beginWatch,
|
|
317
|
+
cancelPrompt,
|
|
318
|
+
dismissError
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export {
|
|
323
|
+
REWARDED_RUNTIME_INJECTION_KEY,
|
|
324
|
+
createRewardedRuntime
|
|
325
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|