@maccesar/titools 5.2.0 → 5.2.2
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/CONTRIBUTING.md +82 -0
- package/README.md +83 -72
- package/agents/ti-pro.md +1 -1
- package/commands/ti-audit.md +2 -2
- package/docs/examples.md +91 -0
- package/docs/knowledge-index.md +57 -0
- package/lib/commands/uninstall.js +19 -16
- package/lib/commands/update.js +1 -1
- package/lib/downloader.js +1 -1
- package/lib/utils.js +2 -0
- package/package.json +4 -2
- package/skills/purgetss/references/multi-density-images.md +1 -1
- package/skills/ti-admob-unified/SKILL.md +21 -7
- package/skills/ti-admob-unified/assets/example/app.js +229 -25
- package/skills/ti-admob-unified/assets/example/lib/ads.js +30 -2
- package/skills/ti-admob-unified/assets/example/library-usage.js +33 -1
- package/skills/ti-admob-unified/references/api-reference.md +39 -16
- package/skills/ti-admob-unified/references/banners.md +9 -9
- package/skills/ti-admob-unified/references/contributing.md +14 -9
- package/skills/ti-admob-unified/references/dependencies.md +31 -25
- package/skills/ti-admob-unified/references/fullscreen.md +43 -28
- package/skills/ti-admob-unified/references/game-monetization.md +138 -7
- package/skills/ti-admob-unified/references/getting-started.md +75 -31
- package/skills/ti-admob-unified/references/index.md +5 -2
- package/skills/ti-admob-unified/references/integration.md +8 -6
- package/skills/ti-admob-unified/references/library.md +41 -10
- package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
- package/skills/ti-admob-unified/references/mediation.md +22 -9
- package/skills/ti-admob-unified/references/migration.md +11 -7
- package/skills/ti-admob-unified/references/module-changelog.md +41 -11
- package/skills/ti-admob-unified/references/privacy.md +35 -8
- package/skills/ti-admob-unified/references/production.md +67 -0
- package/skills/ti-admob-unified/references/rewarded-interstitial.md +101 -0
- package/skills/ti-admob-unified/references/roadmap.md +28 -31
- package/skills/ti-admob-unified/references/source-notes.md +51 -4
- package/skills/ti-admob-unified/references/source-snapshot.json +249 -45
- package/skills/ti-admob-unified/references/third-party-notices.md +4 -4
- package/skills/ti-game/SKILL.md +23 -6
- package/skills/ti-game/references/api.md +34 -30
- package/skills/ti-game/references/debugging-performance.md +18 -9
- package/skills/ti-game/references/examples.md +103 -20
- package/skills/ti-game/references/input.md +101 -0
- package/skills/ti-game/references/patterns.md +39 -15
- package/skills/ti-game/references/physics.md +80 -52
- package/skills/ti-game/references/project-setup.md +118 -56
- package/skills/ti-game/references/recipes-action.md +107 -8
- package/skills/ti-game/references/recipes-adventure-effects.md +41 -9
- package/skills/ti-game/references/roadmap.md +10 -3
- package/skills/ti-game/references/source-notes.md +81 -0
- package/skills/ti-game/references/tilemaps.md +12 -4
- package/skills/ti-synthengine/SKILL.md +81 -95
- package/skills/ti-synthengine/assets/example/README.md +84 -0
- package/skills/ti-synthengine/assets/example/app.js +1178 -0
- package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
- package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
- package/skills/ti-synthengine/references/advanced-examples.md +174 -0
- package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
- package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
- package/skills/ti-synthengine/references/api.md +434 -380
- package/skills/ti-synthengine/references/audio-engine.md +157 -230
- package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
- package/skills/ti-synthengine/references/examples.md +123 -253
- package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
- package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
- package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
- package/skills/ti-synthengine/references/guide.md +188 -156
- package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
- package/skills/ti-synthengine/references/performance-review.json +1146 -0
- package/skills/ti-synthengine/references/physical-validation.json +1457 -0
- package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
- package/skills/ti-synthengine/references/recipes.md +423 -599
- package/skills/ti-synthengine/references/recommendations.md +63 -113
- package/skills/ti-synthengine/references/sound-design.md +134 -258
- package/skills/ti-synthengine/references/source-notes.md +93 -105
- package/skills/ti-synthengine/references/source-snapshot.json +379 -32
- package/skills/ti-synthengine/references/stats-performance.md +64 -20
- package/skills/ti-synthengine/references/studio-validation.json +6202 -0
- package/skills/ti-synthengine/references/troubleshooting.md +109 -121
- package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
- package/skills/ti-synthengine/references/unified-validation.json +314 -0
- package/AGENTS-VERCEL-RESEARCH.md +0 -175
package/lib/utils.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maccesar/titools",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.2",
|
|
4
4
|
"description": "Titanium SDK skills and agents for AI coding assistants, including native game and sound synthesis modules",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -63,7 +63,9 @@
|
|
|
63
63
|
"skills/",
|
|
64
64
|
"agents/",
|
|
65
65
|
"commands/",
|
|
66
|
-
"
|
|
66
|
+
"CONTRIBUTING.md",
|
|
67
|
+
"docs/examples.md",
|
|
68
|
+
"docs/knowledge-index.md"
|
|
67
69
|
],
|
|
68
70
|
"publishConfig": {
|
|
69
71
|
"access": "public"
|
|
@@ -360,7 +360,7 @@ If your source images live elsewhere (e.g. next to your design files in `docs/sc
|
|
|
360
360
|
|
|
361
361
|
```bash
|
|
362
362
|
purgetss images ./docs/screenshots/home-hero.png
|
|
363
|
-
purgetss images /
|
|
363
|
+
purgetss images /path/to/design/banner.svg
|
|
364
364
|
```
|
|
365
365
|
|
|
366
366
|
When the source is outside `purgetss/images/`, subdirectory preservation uses the directory of the source file as the root instead. Processing that external source in Classic does not bootstrap unrelated PurgeTSS folders or config.
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ti-admob-unified
|
|
3
|
-
description: "Use when a Titanium app declares ti.admob.unified or its mediation complements, imports require('ti.admob.unified'), or asks to integrate/configure AdMob banners, interstitials, rewarded or app-open ads, UMP/ATT, privacy options, lifecycle, ad frequency or AppLovin, Meta, InMobi, Unity and Liftoff mediation with this module. Applies to Alloy and Classic on Android/iOS."
|
|
3
|
+
description: "Use when a Titanium app declares ti.admob.unified or its mediation complements, imports require('ti.admob.unified'), or asks to integrate/configure AdMob banners, interstitials, rewarded, rewarded interstitial or app-open ads, UMP/ATT, privacy options, lifecycle, ad frequency or AppLovin, Meta, InMobi, Unity and Liftoff mediation with this module. Applies to Alloy and Classic on Android/iOS."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# ti.admob.unified — ads, privacy and providers
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Use the public **ti.admob.unified 1.0.0** module through its official
|
|
9
|
+
[repository](https://github.com/macCesar/ti.admob.unified),
|
|
10
|
+
[Android release](https://github.com/macCesar/ti.admob.unified/releases/tag/android-1.0.0)
|
|
11
|
+
and [iOS release](https://github.com/macCesar/ti.admob.unified/releases/tag/ios-1.0.0).
|
|
12
|
+
Reviewed released source: `034843299b2984253988e0ffccb9f6a1c91682c9` (2026-10-24).
|
|
13
|
+
The core supports banners, interstitial, rewarded, rewarded interstitial and
|
|
14
|
+
app-open formats; install only the mediation complements the app uses.
|
|
15
|
+
Requires **Titanium 13.4.1.GA**, iOS 15+, Android API 24+ (compile SDK 36).
|
|
16
|
+
Apple packages target iOS, not Mac Catalyst. See [source notes](references/source-notes.md)
|
|
17
|
+
and the [snapshot](references/source-snapshot.json) for provenance and validation
|
|
18
|
+
boundaries. Public module availability does not establish production ad eligibility.
|
|
9
19
|
|
|
10
20
|
## Required workflow
|
|
11
21
|
|
|
@@ -19,9 +29,12 @@ Read the relevant references before proposing calls or configuration. Do not inf
|
|
|
19
29
|
| Consent-first startup, ATT, privacy options, request eligibility | [references/privacy.md](references/privacy.md) |
|
|
20
30
|
| Fixed/adaptive banners, real reserved layout and pause/cleanup | [references/banners.md](references/banners.md) |
|
|
21
31
|
| Interstitial/rewarded/app-open load/show, rewards and cleanup | [references/fullscreen.md](references/fullscreen.md) |
|
|
22
|
-
|
|
|
32
|
+
| Rewarded interstitial introduction, visible Skip, rewards and cancellation | [references/rewarded-interstitial.md](references/rewarded-interstitial.md), [references/fullscreen.md](references/fullscreen.md) |
|
|
33
|
+
| Production IDs, app-ads.txt, store/AdMob readiness and final-build checks | [references/production.md](references/production.md) |
|
|
34
|
+
| Select/install providers, publisher accounts, mappings, diagnostics | [references/mediation.md](references/mediation.md), [references/dependencies.md](references/dependencies.md), [references/mediation-parity.md](references/mediation-parity.md) |
|
|
23
35
|
| Replace legacy modules or internal builds | [references/migration.md](references/migration.md) |
|
|
24
36
|
| Unsupported formats, extending providers, license/distribution | [references/roadmap.md](references/roadmap.md), [references/contributing.md](references/contributing.md), [references/third-party-notices.md](references/third-party-notices.md) |
|
|
37
|
+
| Official docs index and release history | [references/index.md](references/index.md), [references/module-changelog.md](references/module-changelog.md) |
|
|
25
38
|
| Game experience and frequency decisions | [references/game-monetization.md](references/game-monetization.md), plus fullscreen/privacy |
|
|
26
39
|
|
|
27
40
|
Cite factual guidance with `[source: references/<file>.md]`. If the relevant source is unavailable, mark the claim `FROM_MEMORY (unverified):` and do not invent a method or provider setting.
|
|
@@ -30,12 +43,13 @@ Cite factual guidance with `[source: references/<file>.md]`. If the relevant sou
|
|
|
30
43
|
|
|
31
44
|
- Inspect `tiapp.xml`, installed artifacts, project type and existing ad owner before adding another boundary. Use `ti-guides`/`ti-howtos`, Alloy or UI skills as applicable.
|
|
32
45
|
- Prefer the official copy-ready `assets/example/lib/ads.js`: copy unchanged to Alloy `app/lib/ads.js` or Classic `Resources/lib/ads.js`. Its documentation imports `require('lib/ads')`; preserve its path and provenance. `assets/example/library-usage.js` and `assets/example/app.js` are official integration evidence, not navigation templates to paste wholesale.
|
|
33
|
-
- Follow
|
|
46
|
+
- Follow UMP → optional foreground ATT → `canRequestAds()` → initialization/loading. A UMP error can retain prior eligibility; check the gate. `getTrackingAuthorizationStatus()` reads ATT without prompting or initializing. Android reports `applicable:false`; its `authorized:true` convention does not grant general tracking permission. The library defers ATT to `resumed` when needed. Preserve privacy options and named-listener cleanup.
|
|
34
47
|
- Application IDs and ad-unit IDs are different. Use documented test IDs during development; do not switch to production IDs as an incidental fix.
|
|
35
|
-
- Native activity/app lifecycle and SDK banner refresh belong to the module/Google. The app owns tab/window coverage and reserved layout. Do not add refresh timers or duplicate background handlers for banners.
|
|
36
|
-
- Separate
|
|
48
|
+
- Preserve banners during Android's `_closeFromActivityForcedToDestroy` event; ordinary close still destroys the owner. Each banner manager has independent coverage/background state. Native activity/app lifecycle and SDK banner refresh belong to the module/Google. The app owns tab/window coverage and reserved layout. Do not add refresh timers or duplicate background handlers for banners.
|
|
49
|
+
- Separate preloading from deliberate presentation. Loading alone never authorizes showing. An explicit pending Show intent must expire with its presentation context. Coordinate game clock/audio/inputs, no-fill, show failure, close and destroyed screens. Raw `load()`/`show()` are void; manager Boolean returns describe its attempt, not delivery. A stale-size rejection reloads but does not queue manager presentation. `autoReload` is a live close policy that loads another ad, never presents it.
|
|
50
|
+
- Rewarded interstitial requires an accurate reward introduction and a visible Skip option. Present only while that introduction remains valid; credit only `rewarded`, not close/impression/click. Cancel pending introductions on background, lost eligibility, navigation or another fullscreen. Reward handling may need to survive screen changes for an already-presented ad.
|
|
37
51
|
- Install only selected mediation complements on each target platform. All application calls use `ti.admob.unified`. Configure provider accounts and AdMob mappings; neither a packaged adapter nor `ready` diagnostics nor a Google test ad proves provider delivery. Do not invent placeholder keys, manual provider initializers or an `enableMediation` option.
|
|
38
|
-
- Consult the roadmap before suggesting native ads,
|
|
52
|
+
- Consult the roadmap before suggesting native ads, deferred ironSource or a network outside the distributed set. Supporting several formats does not justify enabling all of them in a game.
|
|
39
53
|
|
|
40
54
|
## Acceptance
|
|
41
55
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ti.admob.unified example
|
|
3
3
|
*
|
|
4
4
|
* This app uses only Google's official demo ad unit IDs. It demonstrates:
|
|
5
|
-
* - UMP ->
|
|
5
|
+
* - UMP -> ATT (iOS) -> canRequestAds -> initialization
|
|
6
6
|
* - every ad format the module exposes, one button each
|
|
7
7
|
* - a banner host that reserves its own space
|
|
8
8
|
* - explicit listener and proxy cleanup
|
|
@@ -39,11 +39,14 @@ let preserveConsentBannerSlot = false
|
|
|
39
39
|
let initializationStarted = false
|
|
40
40
|
let initializationComplete = false
|
|
41
41
|
let trackingRequestStarted = false
|
|
42
|
+
let appActive = true
|
|
43
|
+
let trackingPending = false
|
|
42
44
|
let windowClosed = false
|
|
43
45
|
let banner = null
|
|
44
46
|
let activeBannerButton = null
|
|
45
47
|
let interstitial = null
|
|
46
48
|
let rewarded = null
|
|
49
|
+
let rewardedInterstitial = null
|
|
47
50
|
let appOpen = null
|
|
48
51
|
|
|
49
52
|
// Native semantic color names keep existing views in sync with the system theme.
|
|
@@ -59,10 +62,15 @@ const win = Ti.UI.createWindow({
|
|
|
59
62
|
title: 'AdMob Playground',
|
|
60
63
|
barColor: colors.canvas,
|
|
61
64
|
titleAttributes: { color: colors.ink },
|
|
62
|
-
layout: '
|
|
65
|
+
layout: 'vertical',
|
|
63
66
|
extendSafeArea: false
|
|
64
67
|
})
|
|
65
68
|
|
|
69
|
+
// Native vertical layout reserves the HUD's measured height above the viewport.
|
|
70
|
+
const viewport = Ti.UI.createView({
|
|
71
|
+
width: Ti.UI.FILL, height: Ti.UI.FILL, layout: 'composite'
|
|
72
|
+
})
|
|
73
|
+
|
|
66
74
|
const content = Ti.UI.createScrollView({
|
|
67
75
|
top: 0, left: 0, right: 0, bottom: 0,
|
|
68
76
|
layout: 'vertical', contentHeight: Ti.UI.SIZE,
|
|
@@ -96,7 +104,7 @@ masthead.add(Ti.UI.createLabel({
|
|
|
96
104
|
page.add(masthead)
|
|
97
105
|
|
|
98
106
|
const session = Ti.UI.createView({
|
|
99
|
-
top: '
|
|
107
|
+
top: '12dp', bottom: '12dp', left: '24dp', right: '24dp', height: Ti.UI.SIZE,
|
|
100
108
|
layout: 'vertical', backgroundColor: colors.tint, borderRadius: '12dp'
|
|
101
109
|
})
|
|
102
110
|
const sessionLabel = Ti.UI.createLabel({
|
|
@@ -107,12 +115,11 @@ const sessionLabel = Ti.UI.createLabel({
|
|
|
107
115
|
const statusLabel = Ti.UI.createLabel({
|
|
108
116
|
text: 'Requesting privacy information…',
|
|
109
117
|
top: '8dp', left: '16dp', right: '16dp', bottom: '16dp',
|
|
110
|
-
height:
|
|
118
|
+
height: Ti.UI.SIZE, verticalAlign: Ti.UI.TEXT_VERTICAL_ALIGNMENT_TOP, color: colors.ink,
|
|
111
119
|
font: { fontSize: '16dp', fontWeight: 'semibold' }
|
|
112
120
|
})
|
|
113
121
|
session.add(sessionLabel)
|
|
114
122
|
session.add(statusLabel)
|
|
115
|
-
page.add(session)
|
|
116
123
|
|
|
117
124
|
const activity = Ti.UI.createLabel({
|
|
118
125
|
text: 'Session events will appear here.', left: '16dp', right: '16dp',
|
|
@@ -140,6 +147,10 @@ function setBannerHeight(value) {
|
|
|
140
147
|
|
|
141
148
|
let currentSection
|
|
142
149
|
const formatCards = []
|
|
150
|
+
const fullscreenButtons = []
|
|
151
|
+
function cancelPendingFullscreenShows() {
|
|
152
|
+
fullscreenButtons.forEach(button => button.cancelPendingShow())
|
|
153
|
+
}
|
|
143
154
|
function createSection(title, description, grid) {
|
|
144
155
|
page.add(Ti.UI.createLabel({
|
|
145
156
|
text: title, top: '32dp', left: '24dp', right: '24dp', height: Ti.UI.SIZE,
|
|
@@ -190,7 +201,11 @@ function createAdButton(title, handlers) {
|
|
|
190
201
|
accessibilityHint: handlers.description || ''
|
|
191
202
|
})
|
|
192
203
|
let state = 'idle'
|
|
193
|
-
|
|
204
|
+
let pendingShow = false
|
|
205
|
+
let introPending = false
|
|
206
|
+
let introAccepted = false
|
|
207
|
+
let showIntent = 0
|
|
208
|
+
const sizeGuard = fullscreen ? createFullscreenSizeGuard(() => win) : null
|
|
194
209
|
button.creating = function () {
|
|
195
210
|
state = 'creating'
|
|
196
211
|
button.applyProperties({ title: 'Loading…', backgroundColor: colors.quiet, color: colors.muted, enabled: false })
|
|
@@ -201,8 +216,16 @@ function createAdButton(title, handlers) {
|
|
|
201
216
|
title: `Show ${title.toLowerCase()}`, backgroundColor: colors.accent,
|
|
202
217
|
backgroundSelectedColor: colors.pressed, color: colors.surface, enabled: true
|
|
203
218
|
})
|
|
219
|
+
if (pendingShow) presentRequestedAd()
|
|
220
|
+
}
|
|
221
|
+
button.cancelPendingShow = function () {
|
|
222
|
+
pendingShow = false
|
|
223
|
+
introPending = false
|
|
224
|
+
introAccepted = false
|
|
225
|
+
showIntent += 1
|
|
204
226
|
}
|
|
205
227
|
button.reset = function () {
|
|
228
|
+
button.cancelPendingShow()
|
|
206
229
|
state = 'idle'
|
|
207
230
|
button.applyProperties({
|
|
208
231
|
title: isFormat && consentBusy ? 'Waiting for consent…'
|
|
@@ -234,24 +257,59 @@ function createAdButton(title, handlers) {
|
|
|
234
257
|
if (sizeGuard) sizeGuard.recordLoad()
|
|
235
258
|
handlers.onLoad(button)
|
|
236
259
|
}
|
|
260
|
+
function presentRequestedAd() {
|
|
261
|
+
if (!pendingShow || state !== 'ready') return
|
|
262
|
+
if (windowClosed || consentBusy || !appActive || !Admob.canRequestAds()) {
|
|
263
|
+
button.cancelPendingShow()
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
if (handlers.beforeShow && !introAccepted) {
|
|
267
|
+
if (introPending) return
|
|
268
|
+
introPending = true
|
|
269
|
+
const intent = showIntent
|
|
270
|
+
handlers.beforeShow(accepted => {
|
|
271
|
+
if (intent !== showIntent || !pendingShow) return
|
|
272
|
+
introPending = false
|
|
273
|
+
if (!accepted) { button.cancelPendingShow(); return }
|
|
274
|
+
introAccepted = true
|
|
275
|
+
presentRequestedAd()
|
|
276
|
+
})
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
sizeGuard.show(() => {
|
|
280
|
+
pendingShow = false
|
|
281
|
+
state = 'showing'
|
|
282
|
+
button.applyProperties({ title: 'Showing…', enabled: false, backgroundColor: colors.quiet, color: colors.muted })
|
|
283
|
+
handlers.onShow(button)
|
|
284
|
+
}, () => button.load())
|
|
285
|
+
}
|
|
237
286
|
button.addEventListener('click', () => {
|
|
238
|
-
if (consentBusy || state === 'creating' || state === 'showing') return
|
|
239
|
-
if (
|
|
240
|
-
if (windowClosed || !Admob.canRequestAds()) {
|
|
241
|
-
setStatus(`${title} blocked by privacy
|
|
287
|
+
if (consentBusy || state === 'creating' || state === 'showing' || introPending) return
|
|
288
|
+
if (fullscreen) {
|
|
289
|
+
if (windowClosed || !appActive || !Admob.canRequestAds()) {
|
|
290
|
+
setStatus(`${title} blocked by privacy or foreground state`)
|
|
242
291
|
return
|
|
243
292
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}, () => button.load())
|
|
293
|
+
cancelPendingFullscreenShows()
|
|
294
|
+
pendingShow = true
|
|
295
|
+
if (state === 'ready') presentRequestedAd()
|
|
296
|
+
else button.load()
|
|
249
297
|
return
|
|
250
298
|
}
|
|
251
|
-
|
|
299
|
+
button.load()
|
|
252
300
|
})
|
|
301
|
+
if (fullscreen) fullscreenButtons.push(button)
|
|
253
302
|
const parent = card || currentSection
|
|
254
303
|
parent.add(button)
|
|
304
|
+
if (handlers.policyUrl) {
|
|
305
|
+
const policy = Ti.UI.createButton({
|
|
306
|
+
title: 'AdMob rewarded interstitial requirements',
|
|
307
|
+
top: '8dp', left: '20dp', right: '20dp', bottom: '8dp',
|
|
308
|
+
height: Ti.UI.SIZE, color: colors.accent, font: { fontSize: '14dp' }
|
|
309
|
+
})
|
|
310
|
+
policy.addEventListener('click', () => Ti.Platform.openURL(handlers.policyUrl))
|
|
311
|
+
parent.add(policy)
|
|
312
|
+
}
|
|
255
313
|
return button
|
|
256
314
|
}
|
|
257
315
|
|
|
@@ -308,6 +366,7 @@ function loadExampleAds() {
|
|
|
308
366
|
rewardedButton.load()
|
|
309
367
|
appOpenButton.load()
|
|
310
368
|
adaptiveButton.load()
|
|
369
|
+
rewardedInterstitialButton.load()
|
|
311
370
|
adsStoppedForConsent = false
|
|
312
371
|
preserveConsentBannerSlot = false
|
|
313
372
|
}
|
|
@@ -319,10 +378,41 @@ function onTrackingAuthorizationCompleted(event) {
|
|
|
319
378
|
onTrackingAuthorizationCompleted
|
|
320
379
|
)
|
|
321
380
|
Ti.API.info(`ATT status: ${event.status}`)
|
|
381
|
+
Ti.API.info('Current ATT state: ' + JSON.stringify(Admob.getTrackingAuthorizationStatus()))
|
|
322
382
|
initializeAdsOnce()
|
|
323
383
|
updateConsentTools()
|
|
324
384
|
}
|
|
325
385
|
|
|
386
|
+
function requestExampleTracking() {
|
|
387
|
+
if (windowClosed || trackingRequestStarted || (Ti.Platform.osname !== 'android' && !appActive)) return
|
|
388
|
+
trackingRequestStarted = true
|
|
389
|
+
Admob.addEventListener('tracking_authorization_completed', onTrackingAuthorizationCompleted)
|
|
390
|
+
updateConsentTools()
|
|
391
|
+
Admob.requestTrackingAuthorization()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let trackingQueryCount = 0
|
|
395
|
+
function updateTrackingStatus(manual = false) {
|
|
396
|
+
if (manual) trackingQueryCount += 1
|
|
397
|
+
const tracking = Admob.getTrackingAuthorizationStatus()
|
|
398
|
+
const names = ['Not determined', 'Restricted', 'Denied', 'Authorized']
|
|
399
|
+
trackingStatusLabel.text = tracking.applicable
|
|
400
|
+
? `ATT status: ${names[tracking.status] || 'Unknown'} (${tracking.status}). Authorized: ${tracking.authorized}. Applicable: true.`
|
|
401
|
+
: 'ATT status: not applicable on Android (3). Authorized: true is a portable convention, not general tracking permission.'
|
|
402
|
+
trackingStatusLabel.text += `\nLast checked: ${new Date().toLocaleTimeString()}. Manual checks: ${trackingQueryCount}.`
|
|
403
|
+
if (tracking.applicable && tracking.status !== 0) {
|
|
404
|
+
trackingStatusLabel.text += '\nPermission already resolved. Change it in iOS Settings.'
|
|
405
|
+
}
|
|
406
|
+
trackingQueryButton.enabled = !windowClosed && !consentBusy && !trackingRequestStarted
|
|
407
|
+
trackingPermissionButton.enabled = !windowClosed && appActive && !consentBusy && !trackingRequestStarted
|
|
408
|
+
&& !(initializationStarted && !initializationComplete) && tracking.applicable && tracking.status === 0
|
|
409
|
+
trackingPermissionButton.applyProperties({
|
|
410
|
+
backgroundColor: trackingPermissionButton.enabled ? colors.tint : colors.quiet,
|
|
411
|
+
color: trackingPermissionButton.enabled ? colors.accent : colors.muted
|
|
412
|
+
})
|
|
413
|
+
return tracking
|
|
414
|
+
}
|
|
415
|
+
|
|
326
416
|
function continueAfterPrivacyGate(canRequestAds) {
|
|
327
417
|
if (!canRequestAds || initializationStarted || windowClosed) {
|
|
328
418
|
return
|
|
@@ -332,12 +422,23 @@ function continueAfterPrivacyGate(canRequestAds) {
|
|
|
332
422
|
return
|
|
333
423
|
}
|
|
334
424
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
425
|
+
if (Ti.Platform.osname !== 'android' && !appActive) {
|
|
426
|
+
trackingPending = true
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
trackingPending = false
|
|
430
|
+
requestExampleTracking()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function onApplicationPause() {
|
|
434
|
+
appActive = false
|
|
435
|
+
cancelPendingFullscreenShows()
|
|
436
|
+
updateTrackingStatus()
|
|
437
|
+
}
|
|
438
|
+
function onApplicationResumed() {
|
|
439
|
+
appActive = true
|
|
440
|
+
if (trackingPending) continueAfterPrivacyGate(Admob.canRequestAds())
|
|
441
|
+
updateTrackingStatus()
|
|
341
442
|
}
|
|
342
443
|
|
|
343
444
|
// Reopening is separate from initial consent; unknown/error is not NOT_REQUIRED.
|
|
@@ -361,7 +462,7 @@ function updateConsentTools() {
|
|
|
361
462
|
button.enabled = !busy
|
|
362
463
|
button.title = busy ? 'Run test — please wait…' : 'Run test'
|
|
363
464
|
})
|
|
364
|
-
|
|
465
|
+
updateTrackingStatus()
|
|
365
466
|
}
|
|
366
467
|
|
|
367
468
|
function stopAdsForConsent() {
|
|
@@ -369,9 +470,11 @@ function stopAdsForConsent() {
|
|
|
369
470
|
destroyBanner()
|
|
370
471
|
destroyInterstitial()
|
|
371
472
|
destroyRewarded()
|
|
473
|
+
destroyRewardedInterstitial()
|
|
372
474
|
destroyAppOpen()
|
|
373
475
|
interstitialButton.reset()
|
|
374
476
|
rewardedButton.reset()
|
|
477
|
+
rewardedInterstitialButton.reset()
|
|
375
478
|
appOpenButton.reset()
|
|
376
479
|
adsStoppedForConsent = true
|
|
377
480
|
}
|
|
@@ -680,6 +783,32 @@ function onRewardedClosed() {
|
|
|
680
783
|
rewardedButton.load()
|
|
681
784
|
}
|
|
682
785
|
|
|
786
|
+
function destroyRewardedInterstitial() {
|
|
787
|
+
if (!rewardedInterstitial) return
|
|
788
|
+
rewardedInterstitial.removeEventListener(Admob.AD_LOADED, onRewardedInterstitialLoaded)
|
|
789
|
+
rewardedInterstitial.removeEventListener(Admob.AD_FAILED_TO_LOAD, onRewardedInterstitialFailed)
|
|
790
|
+
rewardedInterstitial.removeEventListener(Admob.AD_FAILED_TO_SHOW, onRewardedInterstitialFailed)
|
|
791
|
+
rewardedInterstitial.removeEventListener(Admob.AD_REWARDED, onRewardEarned)
|
|
792
|
+
rewardedInterstitial.removeEventListener(Admob.AD_CLOSED, onRewardedInterstitialClosed)
|
|
793
|
+
rewardedInterstitial.destroy()
|
|
794
|
+
rewardedInterstitial = null
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function onRewardedInterstitialLoaded() {
|
|
798
|
+
setStatus('Rewarded interstitial ready')
|
|
799
|
+
rewardedInterstitialButton.ready()
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function onRewardedInterstitialFailed(event) {
|
|
803
|
+
setStatus(`Rewarded interstitial failed: ${event.error} (${event.code})`)
|
|
804
|
+
rewardedInterstitialButton.failed()
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function onRewardedInterstitialClosed() {
|
|
808
|
+
rewardedInterstitialButton.reset()
|
|
809
|
+
rewardedInterstitialButton.load()
|
|
810
|
+
}
|
|
811
|
+
|
|
683
812
|
function destroyAppOpen() {
|
|
684
813
|
if (!appOpen) {
|
|
685
814
|
return
|
|
@@ -760,6 +889,11 @@ const interstitialButton = createAdButton('Interstitial', {
|
|
|
760
889
|
const rewardedButton = createAdButton('Rewarded', {
|
|
761
890
|
description: 'An opt-in experience that earns an in-app reward.',
|
|
762
891
|
onLoad: () => {
|
|
892
|
+
// Reuse consumed proxies so a delayed SDK reward still reaches its listener.
|
|
893
|
+
if (rewarded && !rewarded.isLoaded()) {
|
|
894
|
+
rewarded.load()
|
|
895
|
+
return
|
|
896
|
+
}
|
|
763
897
|
destroyRewarded()
|
|
764
898
|
rewarded = Admob.createRewarded({
|
|
765
899
|
adUnitId: Admob.TEST_REWARDED,
|
|
@@ -796,6 +930,43 @@ const appOpenButton = createAdButton('App open', {
|
|
|
796
930
|
onShow: () => appOpen.show()
|
|
797
931
|
})
|
|
798
932
|
|
|
933
|
+
const rewardedInterstitialButton = createAdButton('Rewarded interstitial', {
|
|
934
|
+
description: 'Complete this test ad to earn 10 demo coins. AdMob requires an introduction explaining the reward, a clear Skip option before the ad starts, and enough time to decide. Tap Show to choose Watch test ad or Skip.',
|
|
935
|
+
policyUrl: 'https://support.google.com/admob/answer/9884467?hl=en',
|
|
936
|
+
onLoad: () => {
|
|
937
|
+
// Reuse consumed proxies so a delayed SDK reward still reaches its listener.
|
|
938
|
+
if (rewardedInterstitial && !rewardedInterstitial.isLoaded()) {
|
|
939
|
+
rewardedInterstitial.load()
|
|
940
|
+
return
|
|
941
|
+
}
|
|
942
|
+
destroyRewardedInterstitial()
|
|
943
|
+
rewardedInterstitial = Admob.createRewardedInterstitial({
|
|
944
|
+
adUnitId: Admob.TEST_REWARDED_INTERSTITIAL,
|
|
945
|
+
requestAgent: 'ti.admob.unified example', autoReload: false
|
|
946
|
+
})
|
|
947
|
+
rewardedInterstitial.addEventListener(Admob.AD_LOADED, onRewardedInterstitialLoaded)
|
|
948
|
+
rewardedInterstitial.addEventListener(Admob.AD_FAILED_TO_LOAD, onRewardedInterstitialFailed)
|
|
949
|
+
rewardedInterstitial.addEventListener(Admob.AD_FAILED_TO_SHOW, onRewardedInterstitialFailed)
|
|
950
|
+
rewardedInterstitial.addEventListener(Admob.AD_REWARDED, onRewardEarned)
|
|
951
|
+
rewardedInterstitial.addEventListener(Admob.AD_CLOSED, onRewardedInterstitialClosed)
|
|
952
|
+
rewardedInterstitial.load()
|
|
953
|
+
},
|
|
954
|
+
beforeShow: complete => {
|
|
955
|
+
const intro = Ti.UI.createAlertDialog({
|
|
956
|
+
title: 'Test ad reward',
|
|
957
|
+
message: 'Watch this test ad to earn 10 demo coins, or skip and continue without watching.',
|
|
958
|
+
buttonNames: ['Watch test ad', 'Skip'], cancel: 1
|
|
959
|
+
})
|
|
960
|
+
intro.addEventListener('click', event => {
|
|
961
|
+
const watch = event.index === 0
|
|
962
|
+
if (!watch) setStatus('Rewarded interstitial skipped')
|
|
963
|
+
complete(watch)
|
|
964
|
+
})
|
|
965
|
+
intro.show()
|
|
966
|
+
},
|
|
967
|
+
onShow: () => rewardedInterstitial.show()
|
|
968
|
+
})
|
|
969
|
+
|
|
799
970
|
createSection('Banner ads', 'Adaptive starts automatically. Choose a format for the banner slot.', true)
|
|
800
971
|
|
|
801
972
|
const fixedButton = createAdButton('Fixed banner', {
|
|
@@ -869,6 +1040,30 @@ currentSection.add(privacyNote)
|
|
|
869
1040
|
|
|
870
1041
|
setPrivacyOptionsAvailable(undefined)
|
|
871
1042
|
|
|
1043
|
+
createSection('Tracking transparency', 'Check ATT without a prompt. Request it only when the app uses tracking. UMP may already handle the request.', false)
|
|
1044
|
+
const trackingStatusLabel = Ti.UI.createLabel({
|
|
1045
|
+
top: '8dp', left: '8dp', right: '8dp', height: Ti.UI.SIZE,
|
|
1046
|
+
color: colors.muted, font: { fontSize: '13dp' }, text: 'ATT status: checking…'
|
|
1047
|
+
})
|
|
1048
|
+
currentSection.add(trackingStatusLabel)
|
|
1049
|
+
const trackingQueryButton = createAdButton('Check ATT status', {
|
|
1050
|
+
privacy: true,
|
|
1051
|
+
onLoad: button => {
|
|
1052
|
+
button.reset()
|
|
1053
|
+
updateTrackingStatus(true)
|
|
1054
|
+
setStatus('ATT status refreshed without requesting permission')
|
|
1055
|
+
}
|
|
1056
|
+
})
|
|
1057
|
+
const trackingPermissionButton = createAdButton('Request ATT permission', {
|
|
1058
|
+
privacy: true,
|
|
1059
|
+
onLoad: button => {
|
|
1060
|
+
button.reset()
|
|
1061
|
+
const tracking = updateTrackingStatus()
|
|
1062
|
+
if (tracking.applicable && tracking.status === 0 && appActive) requestExampleTracking()
|
|
1063
|
+
}
|
|
1064
|
+
})
|
|
1065
|
+
updateTrackingStatus()
|
|
1066
|
+
|
|
872
1067
|
if (consentTestingEnabled) {
|
|
873
1068
|
createSection('Consent demo', 'Choose a scenario and run it. Testing clears the previous consent choice.', false)
|
|
874
1069
|
currentSection.add(Ti.UI.createLabel({
|
|
@@ -967,14 +1162,22 @@ Admob.addEventListener(
|
|
|
967
1162
|
)
|
|
968
1163
|
Admob.addEventListener('ad_inspector_closed', onAdInspectorClosed)
|
|
969
1164
|
|
|
970
|
-
|
|
971
|
-
|
|
1165
|
+
viewport.add(content)
|
|
1166
|
+
viewport.add(bannerHost)
|
|
1167
|
+
win.add(session)
|
|
1168
|
+
win.add(viewport)
|
|
1169
|
+
Ti.App.addEventListener('pause', onApplicationPause)
|
|
1170
|
+
Ti.App.addEventListener('resumed', onApplicationResumed)
|
|
972
1171
|
|
|
973
1172
|
// On Android, Titanium also fires close while it recreates the activity, for
|
|
974
1173
|
// example on a light/dark switch. The window survives, so its ads must too.
|
|
975
1174
|
win.addEventListener('close', event => {
|
|
976
1175
|
if (event._closeFromActivityForcedToDestroy) return
|
|
977
1176
|
windowClosed = true
|
|
1177
|
+
cancelPendingFullscreenShows()
|
|
1178
|
+
trackingPending = false
|
|
1179
|
+
Ti.App.removeEventListener('pause', onApplicationPause)
|
|
1180
|
+
Ti.App.removeEventListener('resumed', onApplicationResumed)
|
|
978
1181
|
content.removeEventListener('postlayout', layoutPage)
|
|
979
1182
|
|
|
980
1183
|
Admob.removeEventListener('module_initialized', onModuleInitialized)
|
|
@@ -993,6 +1196,7 @@ win.addEventListener('close', event => {
|
|
|
993
1196
|
destroyBanner()
|
|
994
1197
|
destroyInterstitial()
|
|
995
1198
|
destroyRewarded()
|
|
1199
|
+
destroyRewardedInterstitial()
|
|
996
1200
|
destroyAppOpen()
|
|
997
1201
|
})
|
|
998
1202
|
|
|
@@ -95,6 +95,7 @@ async function initializeIfAllowed() {
|
|
|
95
95
|
* options.consent: dictionary passed to requestConsent()
|
|
96
96
|
* options.requestTrackingAuthorization: true only when the app has a real ATT use case
|
|
97
97
|
* options.initialization: dictionary passed to initialize()
|
|
98
|
+
* Call start() while active. If UMP spans an iOS interruption, ATT waits for resumed.
|
|
98
99
|
*/
|
|
99
100
|
function start(options) {
|
|
100
101
|
if (startup) {
|
|
@@ -103,6 +104,18 @@ function start(options) {
|
|
|
103
104
|
|
|
104
105
|
startupOptions = options || {}
|
|
105
106
|
startup = (async () => {
|
|
107
|
+
const watchForeground = startupOptions.requestTrackingAuthorization === true && Ti.Platform.osname !== 'android'
|
|
108
|
+
let active = true
|
|
109
|
+
let resumeTracking = null
|
|
110
|
+
function onPause() { active = false }
|
|
111
|
+
function onResumed() {
|
|
112
|
+
active = true
|
|
113
|
+
if (resumeTracking) resumeTracking()
|
|
114
|
+
}
|
|
115
|
+
if (watchForeground) {
|
|
116
|
+
Ti.App.addEventListener('pause', onPause)
|
|
117
|
+
Ti.App.addEventListener('resumed', onResumed)
|
|
118
|
+
}
|
|
106
119
|
const consent = await waitForEvent('consent_complete', () => {
|
|
107
120
|
Admob.requestConsent(startupOptions.consent || {})
|
|
108
121
|
})
|
|
@@ -113,6 +126,11 @@ function start(options) {
|
|
|
113
126
|
|
|
114
127
|
let tracking = null
|
|
115
128
|
if (startupOptions.requestTrackingAuthorization === true) {
|
|
129
|
+
if (watchForeground) {
|
|
130
|
+
if (!active) await new Promise(resolve => { resumeTracking = resolve })
|
|
131
|
+
Ti.App.removeEventListener('pause', onPause)
|
|
132
|
+
Ti.App.removeEventListener('resumed', onResumed)
|
|
133
|
+
}
|
|
116
134
|
tracking = await waitForEvent('tracking_authorization_completed', () => {
|
|
117
135
|
Admob.requestTrackingAuthorization()
|
|
118
136
|
})
|
|
@@ -409,7 +427,12 @@ function fullscreenProperties(options) {
|
|
|
409
427
|
function createFullscreenSizeGuard(getSize) {
|
|
410
428
|
let requestedSize = null
|
|
411
429
|
function sizeKey() {
|
|
412
|
-
const
|
|
430
|
+
const geometry = getSize()
|
|
431
|
+
// Window.size can change while Android restores bars after a fullscreen ad.
|
|
432
|
+
// Outer window bounds stay stable and still track rotation and split view.
|
|
433
|
+
const rect = geometry && geometry.rect
|
|
434
|
+
const size = rect && rect.width > 0 && rect.height > 0
|
|
435
|
+
? rect : (geometry && geometry.size) || geometry
|
|
413
436
|
return size && size.width > 0 && size.height > 0
|
|
414
437
|
? `${Math.round(size.width)}x${Math.round(size.height)}` : null
|
|
415
438
|
}
|
|
@@ -437,7 +460,7 @@ function createFullscreen(options, factoryName) {
|
|
|
437
460
|
let destroyed = false
|
|
438
461
|
let loadRequested = options.autoLoad === true
|
|
439
462
|
let loading = false
|
|
440
|
-
const sizeGuard = createFullscreenSizeGuard(() =>
|
|
463
|
+
const sizeGuard = createFullscreenSizeGuard(() => options.window || Ti.UI.currentWindow)
|
|
441
464
|
|
|
442
465
|
function detach(destroyNative) {
|
|
443
466
|
if (!proxy) {
|
|
@@ -565,6 +588,10 @@ function createRewarded(options) {
|
|
|
565
588
|
return createFullscreen(options, 'createRewarded')
|
|
566
589
|
}
|
|
567
590
|
|
|
591
|
+
function createRewardedInterstitial(options) {
|
|
592
|
+
return createFullscreen(options, 'createRewardedInterstitial')
|
|
593
|
+
}
|
|
594
|
+
|
|
568
595
|
function createAppOpen(options) {
|
|
569
596
|
return createFullscreen(options, 'createAppOpen')
|
|
570
597
|
}
|
|
@@ -613,6 +640,7 @@ module.exports = {
|
|
|
613
640
|
createFullscreenSizeGuard,
|
|
614
641
|
createInterstitial,
|
|
615
642
|
createRewarded,
|
|
643
|
+
createRewardedInterstitial,
|
|
616
644
|
createAppOpen,
|
|
617
645
|
destroyAll
|
|
618
646
|
}
|