@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
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { reactive } from "vue";
|
|
2
|
+
import { createHttpClient } from "@jskit-ai/http-runtime/client";
|
|
3
|
+
|
|
4
|
+
const GOOGLE_REWARDED_RUNTIME_INJECTION_KEY = Symbol("google-rewarded.web.runtime");
|
|
5
|
+
const GOOGLE_REWARDED_CONFIGURATION_REASONS = new Set([
|
|
6
|
+
"rule-not-configured",
|
|
7
|
+
"provider-not-configured"
|
|
8
|
+
]);
|
|
9
|
+
const GOOGLE_REWARDED_NON_BLOCKING_REASONS = new Set([
|
|
10
|
+
"already-unlocked",
|
|
11
|
+
"cooldown-active",
|
|
12
|
+
"daily-limit-reached",
|
|
13
|
+
...GOOGLE_REWARDED_CONFIGURATION_REASONS
|
|
14
|
+
]);
|
|
15
|
+
const googleRewardedHttpClient = createHttpClient({
|
|
16
|
+
credentials: "include",
|
|
17
|
+
csrf: {
|
|
18
|
+
sessionPath: "/api/session"
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
let gptLoadPromise = null;
|
|
23
|
+
let gptServicesEnabled = false;
|
|
24
|
+
|
|
25
|
+
function createInitialState() {
|
|
26
|
+
return {
|
|
27
|
+
open: false,
|
|
28
|
+
phase: "idle",
|
|
29
|
+
errorMessage: "",
|
|
30
|
+
gateState: null,
|
|
31
|
+
session: null,
|
|
32
|
+
request: null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function applyState(target, source) {
|
|
37
|
+
for (const key of Object.keys(target)) {
|
|
38
|
+
if (!Object.hasOwn(source, key)) {
|
|
39
|
+
delete target[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const [key, value] of Object.entries(source)) {
|
|
43
|
+
target[key] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeWorkspaceSlug(value = "") {
|
|
48
|
+
return String(value || "").trim().toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isWellFormedGateState(gateState = null) {
|
|
52
|
+
if (!gateState ||
|
|
53
|
+
typeof gateState !== "object" ||
|
|
54
|
+
typeof gateState.enabled !== "boolean" ||
|
|
55
|
+
typeof gateState.blocked !== "boolean") {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (gateState.blocked === true) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const reason = String(gateState.reason || "").trim().toLowerCase();
|
|
64
|
+
return Boolean(gateState.unlock) || GOOGLE_REWARDED_NON_BLOCKING_REASONS.has(reason);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildApiPath(workspaceSlug = "", action = "", query = null) {
|
|
68
|
+
const normalizedWorkspaceSlug = normalizeWorkspaceSlug(workspaceSlug);
|
|
69
|
+
const pathname = `/api/w/${encodeURIComponent(normalizedWorkspaceSlug)}/google-rewarded/${action}`;
|
|
70
|
+
if (!(query instanceof URLSearchParams) || [...query.keys()].length < 1) {
|
|
71
|
+
return pathname;
|
|
72
|
+
}
|
|
73
|
+
return `${pathname}?${query.toString()}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function ensureGooglePublisherTagLoaded() {
|
|
77
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
78
|
+
throw new Error("Google rewarded runtime requires a browser environment.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (window.googletag?.apiReady === true) {
|
|
82
|
+
return window.googletag;
|
|
83
|
+
}
|
|
84
|
+
if (gptLoadPromise) {
|
|
85
|
+
return gptLoadPromise;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
window.googletag ||= { cmd: [] };
|
|
89
|
+
gptLoadPromise = new Promise((resolve, reject) => {
|
|
90
|
+
const existingScript = document.querySelector('script[data-jskit-google-rewarded-gpt="true"]');
|
|
91
|
+
if (existingScript) {
|
|
92
|
+
existingScript.addEventListener("load", () => resolve(window.googletag), { once: true });
|
|
93
|
+
existingScript.addEventListener("error", () => reject(new Error("Failed to load Google Publisher Tag.")), { once: true });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const script = document.createElement("script");
|
|
98
|
+
script.async = true;
|
|
99
|
+
script.src = "https://securepubads.g.doubleclick.net/tag/js/gpt.js";
|
|
100
|
+
script.dataset.jskitGoogleRewardedGpt = "true";
|
|
101
|
+
script.addEventListener("load", () => resolve(window.googletag), { once: true });
|
|
102
|
+
script.addEventListener("error", () => reject(new Error("Failed to load Google Publisher Tag.")), { once: true });
|
|
103
|
+
document.head.appendChild(script);
|
|
104
|
+
}).catch((error) => {
|
|
105
|
+
gptLoadPromise = null;
|
|
106
|
+
throw error;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return gptLoadPromise;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function launchRewardedSlot({
|
|
113
|
+
adUnitPath = "",
|
|
114
|
+
onReady = null,
|
|
115
|
+
onGranted = null,
|
|
116
|
+
onClosed = null,
|
|
117
|
+
onUnavailable = null
|
|
118
|
+
} = {}) {
|
|
119
|
+
return ensureGooglePublisherTagLoaded().then((googletag) =>
|
|
120
|
+
new Promise((resolve, reject) => {
|
|
121
|
+
googletag.cmd.push(() => {
|
|
122
|
+
const pubads = googletag.pubads();
|
|
123
|
+
const rewardedFormat = googletag?.enums?.OutOfPageFormat?.REWARDED;
|
|
124
|
+
if (!rewardedFormat) {
|
|
125
|
+
reject(new Error("Google rewarded ads are not supported by this GPT build."));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const slot = googletag.defineOutOfPageSlot(adUnitPath, rewardedFormat);
|
|
130
|
+
if (!slot) {
|
|
131
|
+
Promise.resolve(onUnavailable?.()).finally(() => {
|
|
132
|
+
reject(new Error("Google rewarded ads are not available on this page."));
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let readySeen = false;
|
|
138
|
+
let settled = false;
|
|
139
|
+
|
|
140
|
+
const cleanup = () => {
|
|
141
|
+
if (typeof pubads.removeEventListener === "function") {
|
|
142
|
+
pubads.removeEventListener("rewardedSlotReady", handleReady);
|
|
143
|
+
pubads.removeEventListener("rewardedSlotGranted", handleGranted);
|
|
144
|
+
pubads.removeEventListener("rewardedSlotClosed", handleClosed);
|
|
145
|
+
}
|
|
146
|
+
if (typeof googletag.destroySlots === "function") {
|
|
147
|
+
googletag.destroySlots([slot]);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const handleReady = async (event) => {
|
|
152
|
+
if (event?.slot !== slot || settled) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
readySeen = true;
|
|
156
|
+
await Promise.resolve(onReady?.());
|
|
157
|
+
if (typeof event.makeRewardedVisible === "function") {
|
|
158
|
+
event.makeRewardedVisible();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleGranted = async (event) => {
|
|
163
|
+
if (event?.slot !== slot || settled) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
await Promise.resolve(onGranted?.());
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleClosed = async (event) => {
|
|
170
|
+
if (event?.slot !== slot || settled) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
settled = true;
|
|
174
|
+
cleanup();
|
|
175
|
+
try {
|
|
176
|
+
await Promise.resolve(onClosed?.());
|
|
177
|
+
resolve();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
reject(error);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
pubads.addEventListener("rewardedSlotReady", handleReady);
|
|
184
|
+
pubads.addEventListener("rewardedSlotGranted", handleGranted);
|
|
185
|
+
pubads.addEventListener("rewardedSlotClosed", handleClosed);
|
|
186
|
+
slot.addService(pubads);
|
|
187
|
+
|
|
188
|
+
if (!gptServicesEnabled) {
|
|
189
|
+
googletag.enableServices();
|
|
190
|
+
gptServicesEnabled = true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
googletag.display(slot);
|
|
194
|
+
window.setTimeout(() => {
|
|
195
|
+
if (settled || readySeen) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
settled = true;
|
|
199
|
+
cleanup();
|
|
200
|
+
Promise.resolve(onUnavailable?.()).finally(() => {
|
|
201
|
+
reject(new Error("No rewarded ad was available."));
|
|
202
|
+
});
|
|
203
|
+
}, 10000);
|
|
204
|
+
});
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function createGoogleRewardedRuntime() {
|
|
210
|
+
const state = reactive(createInitialState());
|
|
211
|
+
let pendingResolve = null;
|
|
212
|
+
let activeGrantPromise = null;
|
|
213
|
+
|
|
214
|
+
function resetState() {
|
|
215
|
+
activeGrantPromise = null;
|
|
216
|
+
pendingResolve = null;
|
|
217
|
+
applyState(state, createInitialState());
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function settle(result) {
|
|
221
|
+
const resolver = pendingResolve;
|
|
222
|
+
resetState();
|
|
223
|
+
if (typeof resolver === "function") {
|
|
224
|
+
resolver(result);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function requestCurrent(input = {}) {
|
|
229
|
+
const params = new URLSearchParams({
|
|
230
|
+
gateKey: String(input.gateKey || "")
|
|
231
|
+
});
|
|
232
|
+
return googleRewardedHttpClient.request(
|
|
233
|
+
buildApiPath(input.workspaceSlug, "current", params),
|
|
234
|
+
{
|
|
235
|
+
method: "GET"
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function requestStart(input = {}) {
|
|
241
|
+
return googleRewardedHttpClient.request(
|
|
242
|
+
buildApiPath(input.workspaceSlug, "start"),
|
|
243
|
+
{
|
|
244
|
+
method: "POST",
|
|
245
|
+
body: {
|
|
246
|
+
gateKey: input.gateKey
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function requestGrant(input = {}) {
|
|
253
|
+
return googleRewardedHttpClient.request(
|
|
254
|
+
buildApiPath(input.workspaceSlug, "grant"),
|
|
255
|
+
{
|
|
256
|
+
method: "POST",
|
|
257
|
+
body: {
|
|
258
|
+
sessionId: input.sessionId
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function requestClose(input = {}) {
|
|
265
|
+
return googleRewardedHttpClient.request(
|
|
266
|
+
buildApiPath(input.workspaceSlug, "close"),
|
|
267
|
+
{
|
|
268
|
+
method: "POST",
|
|
269
|
+
body: {
|
|
270
|
+
sessionId: input.sessionId
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function requireUnlock(request = {}) {
|
|
277
|
+
const gateKey = String(request?.gateKey || "").trim();
|
|
278
|
+
const workspaceSlug = normalizeWorkspaceSlug(request?.workspaceSlug);
|
|
279
|
+
|
|
280
|
+
if (!gateKey) {
|
|
281
|
+
throw new Error("requireUnlock requires gateKey.");
|
|
282
|
+
}
|
|
283
|
+
if (!workspaceSlug) {
|
|
284
|
+
throw new Error("requireUnlock requires workspaceSlug.");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const gateState = await requestCurrent({
|
|
288
|
+
gateKey,
|
|
289
|
+
workspaceSlug
|
|
290
|
+
});
|
|
291
|
+
if (!isWellFormedGateState(gateState)) {
|
|
292
|
+
throw new Error("Google rewarded gate returned an invalid state.");
|
|
293
|
+
}
|
|
294
|
+
const alreadyUnlocked = gateState?.blocked === false && gateState?.unlock;
|
|
295
|
+
if (!gateState?.enabled || !gateState?.blocked) {
|
|
296
|
+
return {
|
|
297
|
+
granted: Boolean(alreadyUnlocked),
|
|
298
|
+
state: gateState
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (pendingResolve) {
|
|
303
|
+
throw new Error("A Google rewarded gate is already active.");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
applyState(state, {
|
|
307
|
+
open: true,
|
|
308
|
+
phase: "prompt",
|
|
309
|
+
errorMessage: "",
|
|
310
|
+
gateState,
|
|
311
|
+
session: null,
|
|
312
|
+
request: {
|
|
313
|
+
gateKey,
|
|
314
|
+
workspaceSlug
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return new Promise((resolve) => {
|
|
319
|
+
pendingResolve = resolve;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function beginWatch() {
|
|
324
|
+
if (!state.request || state.phase !== "prompt") {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
applyState(state, {
|
|
329
|
+
...state,
|
|
330
|
+
phase: "loading"
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const startState = await requestStart(state.request);
|
|
335
|
+
if (!startState?.session || !startState?.providerConfig?.adUnitPath) {
|
|
336
|
+
settle({
|
|
337
|
+
granted: false,
|
|
338
|
+
state: startState
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
applyState(state, {
|
|
344
|
+
...state,
|
|
345
|
+
gateState: startState,
|
|
346
|
+
session: startState.session,
|
|
347
|
+
errorMessage: ""
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await launchRewardedSlot({
|
|
351
|
+
adUnitPath: startState.providerConfig.adUnitPath,
|
|
352
|
+
onReady() {
|
|
353
|
+
applyState(state, {
|
|
354
|
+
...state,
|
|
355
|
+
phase: "showing-ad"
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
async onGranted() {
|
|
359
|
+
activeGrantPromise = requestGrant({
|
|
360
|
+
workspaceSlug: state.request.workspaceSlug,
|
|
361
|
+
sessionId: state.session?.id
|
|
362
|
+
});
|
|
363
|
+
const grantResult = await activeGrantPromise;
|
|
364
|
+
applyState(state, {
|
|
365
|
+
...state,
|
|
366
|
+
phase: "granted",
|
|
367
|
+
gateState: {
|
|
368
|
+
...state.gateState,
|
|
369
|
+
...grantResult,
|
|
370
|
+
blocked: false,
|
|
371
|
+
unlock: grantResult.unlock || null
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
},
|
|
375
|
+
async onClosed() {
|
|
376
|
+
if (activeGrantPromise) {
|
|
377
|
+
const grantResult = await activeGrantPromise;
|
|
378
|
+
settle({
|
|
379
|
+
granted: true,
|
|
380
|
+
state: {
|
|
381
|
+
...state.gateState,
|
|
382
|
+
...grantResult,
|
|
383
|
+
blocked: false,
|
|
384
|
+
unlock: grantResult.unlock || null
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const closeResult = await requestClose({
|
|
391
|
+
workspaceSlug: state.request.workspaceSlug,
|
|
392
|
+
sessionId: state.session?.id
|
|
393
|
+
});
|
|
394
|
+
settle({
|
|
395
|
+
granted: false,
|
|
396
|
+
state: {
|
|
397
|
+
...state.gateState,
|
|
398
|
+
...closeResult,
|
|
399
|
+
blocked: true
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
onUnavailable() {
|
|
404
|
+
applyState(state, {
|
|
405
|
+
...state,
|
|
406
|
+
phase: "error",
|
|
407
|
+
errorMessage: "No rewarded ad was available right now. Please try again later."
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
} catch (error) {
|
|
412
|
+
applyState(state, {
|
|
413
|
+
...state,
|
|
414
|
+
phase: "error",
|
|
415
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function cancelPrompt() {
|
|
421
|
+
if (!state.request) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (state.session?.id) {
|
|
426
|
+
try {
|
|
427
|
+
await requestClose({
|
|
428
|
+
workspaceSlug: state.request.workspaceSlug,
|
|
429
|
+
sessionId: state.session.id
|
|
430
|
+
});
|
|
431
|
+
} catch {
|
|
432
|
+
// Preserve the original user intent even if cleanup fails.
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
settle({
|
|
437
|
+
granted: false,
|
|
438
|
+
state: state.gateState
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function dismissError() {
|
|
443
|
+
await cancelPrompt();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return Object.freeze({
|
|
447
|
+
state,
|
|
448
|
+
requireUnlock,
|
|
449
|
+
beginWatch,
|
|
450
|
+
cancelPrompt,
|
|
451
|
+
dismissError
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export {
|
|
456
|
+
GOOGLE_REWARDED_RUNTIME_INJECTION_KEY,
|
|
457
|
+
createGoogleRewardedRuntime
|
|
458
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|