@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.
@@ -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 {};