@maccesar/titools 5.1.1 → 5.2.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.
Files changed (98) hide show
  1. package/CONTRIBUTING.md +69 -0
  2. package/README.md +112 -72
  3. package/agents/ti-pro.md +5 -1
  4. package/commands/ti-audit.md +5 -1
  5. package/docs/examples.md +91 -0
  6. package/docs/knowledge-index.md +57 -0
  7. package/lib/config.js +2 -0
  8. package/lib/downloader.js +1 -1
  9. package/package.json +4 -2
  10. package/skills/purgetss/references/multi-density-images.md +1 -1
  11. package/skills/ti-admob-unified/SKILL.md +58 -0
  12. package/skills/ti-admob-unified/agents/openai.yaml +4 -0
  13. package/skills/ti-admob-unified/assets/example/app.js +1207 -0
  14. package/skills/ti-admob-unified/assets/example/lib/ads.js +646 -0
  15. package/skills/ti-admob-unified/assets/example/library-usage.js +136 -0
  16. package/skills/ti-admob-unified/references/api-reference.md +615 -0
  17. package/skills/ti-admob-unified/references/banners.md +435 -0
  18. package/skills/ti-admob-unified/references/contributing.md +233 -0
  19. package/skills/ti-admob-unified/references/dependencies.md +91 -0
  20. package/skills/ti-admob-unified/references/fullscreen.md +238 -0
  21. package/skills/ti-admob-unified/references/game-monetization.md +148 -0
  22. package/skills/ti-admob-unified/references/getting-started.md +319 -0
  23. package/skills/ti-admob-unified/references/index.md +33 -0
  24. package/skills/ti-admob-unified/references/integration.md +87 -0
  25. package/skills/ti-admob-unified/references/library.md +220 -0
  26. package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
  27. package/skills/ti-admob-unified/references/mediation.md +145 -0
  28. package/skills/ti-admob-unified/references/migration.md +259 -0
  29. package/skills/ti-admob-unified/references/module-changelog.md +84 -0
  30. package/skills/ti-admob-unified/references/module-license.txt +21 -0
  31. package/skills/ti-admob-unified/references/privacy.md +281 -0
  32. package/skills/ti-admob-unified/references/production.md +67 -0
  33. package/skills/ti-admob-unified/references/rewarded-interstitial.md +101 -0
  34. package/skills/ti-admob-unified/references/roadmap.md +143 -0
  35. package/skills/ti-admob-unified/references/source-notes.md +80 -0
  36. package/skills/ti-admob-unified/references/source-snapshot.json +644 -0
  37. package/skills/ti-admob-unified/references/third-party-notices.md +22 -0
  38. package/skills/ti-game/SKILL.md +31 -5
  39. package/skills/ti-game/references/api.md +36 -24
  40. package/skills/ti-game/references/debugging-performance.md +22 -7
  41. package/skills/ti-game/references/examples.md +113 -20
  42. package/skills/ti-game/references/input.md +101 -0
  43. package/skills/ti-game/references/patterns.md +56 -10
  44. package/skills/ti-game/references/physics.md +530 -0
  45. package/skills/ti-game/references/project-setup.md +131 -47
  46. package/skills/ti-game/references/recipes-action.md +149 -18
  47. package/skills/ti-game/references/recipes-adventure-effects.md +47 -4
  48. package/skills/ti-game/references/roadmap.md +11 -4
  49. package/skills/ti-game/references/source-notes.md +81 -0
  50. package/skills/ti-game/references/tilemaps.md +13 -4
  51. package/skills/ti-game-assets/SKILL.md +53 -0
  52. package/skills/ti-game-assets/agents/openai.yaml +4 -0
  53. package/skills/ti-game-assets/references/alpha.md +26 -0
  54. package/skills/ti-game-assets/references/art-direction.md +37 -0
  55. package/skills/ti-game-assets/references/integration.md +49 -0
  56. package/skills/ti-game-assets/references/manifest.example.json +429 -0
  57. package/skills/ti-game-assets/references/manifest.md +39 -0
  58. package/skills/ti-game-assets/references/offline-audio.md +33 -0
  59. package/skills/ti-game-assets/references/prompts.md +33 -0
  60. package/skills/ti-game-assets/references/repeating-textures.md +17 -0
  61. package/skills/ti-game-assets/references/responsible-ads.md +31 -0
  62. package/skills/ti-game-assets/references/sound-assets.md +42 -0
  63. package/skills/ti-game-assets/references/vector-sources.md +21 -0
  64. package/skills/ti-game-assets/scripts/alpha_tools.py +160 -0
  65. package/skills/ti-game-assets/scripts/requirements.txt +1 -0
  66. package/skills/ti-game-assets/scripts/validate_manifest.py +267 -0
  67. package/skills/ti-synthengine/SKILL.md +77 -67
  68. package/skills/ti-synthengine/assets/example/README.md +84 -0
  69. package/skills/ti-synthengine/assets/example/app.js +1178 -0
  70. package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
  71. package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
  72. package/skills/ti-synthengine/references/advanced-examples.md +174 -0
  73. package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
  74. package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
  75. package/skills/ti-synthengine/references/api.md +573 -153
  76. package/skills/ti-synthengine/references/audio-engine.md +272 -124
  77. package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
  78. package/skills/ti-synthengine/references/examples.md +355 -106
  79. package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
  80. package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
  81. package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
  82. package/skills/ti-synthengine/references/guide.md +426 -75
  83. package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
  84. package/skills/ti-synthengine/references/performance-review.json +1146 -0
  85. package/skills/ti-synthengine/references/physical-validation.json +1457 -0
  86. package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
  87. package/skills/ti-synthengine/references/recipes.md +434 -420
  88. package/skills/ti-synthengine/references/recommendations.md +72 -85
  89. package/skills/ti-synthengine/references/sound-design.md +375 -155
  90. package/skills/ti-synthengine/references/source-notes.md +109 -0
  91. package/skills/ti-synthengine/references/source-snapshot.json +452 -0
  92. package/skills/ti-synthengine/references/stats-performance.json +2992 -0
  93. package/skills/ti-synthengine/references/stats-performance.md +82 -0
  94. package/skills/ti-synthengine/references/studio-validation.json +6202 -0
  95. package/skills/ti-synthengine/references/troubleshooting.md +123 -78
  96. package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
  97. package/skills/ti-synthengine/references/unified-validation.json +314 -0
  98. package/AGENTS-VERCEL-RESEARCH.md +0 -175
@@ -0,0 +1,1207 @@
1
+ /**
2
+ * ti.admob.unified example
3
+ *
4
+ * This app uses only Google's official demo ad unit IDs. It demonstrates:
5
+ * - UMP -> ATT (iOS) -> canRequestAds -> initialization
6
+ * - every ad format the module exposes, one button each
7
+ * - a banner host that reserves its own space
8
+ * - explicit listener and proxy cleanup
9
+ *
10
+ * Fullscreen ads preload after privacy and initialization, then reload on close.
11
+ * Showing an ad always requires an explicit tap. The adaptive banner starts automatically.
12
+ * The native layout adapts to the available window width, including split view.
13
+ *
14
+ * It intentionally contains no production ad unit IDs, mediation, or manual
15
+ * banner refresh timers.
16
+ */
17
+
18
+ const Admob = require('ti.admob.unified')
19
+ const { createFullscreenSizeGuard } = require('lib/ads')
20
+
21
+ // Demo-only settings. No personal test-device identifiers ship in this file.
22
+ const consentTestingEnabled = Ti.App.deployType !== 'production'
23
+ const consentModes = {
24
+ normal: { label: 'Normal', geography: null },
25
+ eea: { label: 'EEA', geography: Admob.DEBUG_GEOGRAPHY_EEA },
26
+ us: { label: 'Regulated US state', geography: Admob.DEBUG_GEOGRAPHY_REGULATED_US_STATE },
27
+ other: { label: 'Other', geography: Admob.DEBUG_GEOGRAPHY_OTHER }
28
+ }
29
+ let consentMode = consentTestingEnabled ? Ti.App.Properties.getString('demo.consentMode', 'normal') : 'normal'
30
+ if (!consentModes[consentMode]) consentMode = 'normal'
31
+ let consentBusy = false
32
+ let adsStoppedForConsent = false
33
+ let consentDeviceInput
34
+ let consentDemoFeedback
35
+ const consentTestButtons = []
36
+ let openUSOptionsAfterRequest = false
37
+ let preserveConsentBannerSlot = false
38
+
39
+ let initializationStarted = false
40
+ let initializationComplete = false
41
+ let trackingRequestStarted = false
42
+ let appActive = true
43
+ let trackingPending = false
44
+ let windowClosed = false
45
+ let banner = null
46
+ let activeBannerButton = null
47
+ let interstitial = null
48
+ let rewarded = null
49
+ let rewardedInterstitial = null
50
+ let appOpen = null
51
+
52
+ // Native semantic color names keep existing views in sync with the system theme.
53
+ // Copy semantic.colors.json beside app.js in a classic app's Resources directory.
54
+ const colors = {
55
+ canvas: 'playgroundCanvas', surface: 'playgroundSurface', ink: 'playgroundInk', muted: 'playgroundMuted',
56
+ accent: 'playgroundAccent', pressed: 'playgroundPressed', tint: 'playgroundTint', line: 'playgroundLine',
57
+ quiet: 'playgroundQuiet', error: 'playgroundError'
58
+ }
59
+ const isAndroid = Ti.Platform.osname === 'android'
60
+ const win = Ti.UI.createWindow({
61
+ backgroundColor: colors.canvas,
62
+ title: 'AdMob Playground',
63
+ barColor: colors.canvas,
64
+ titleAttributes: { color: colors.ink },
65
+ layout: 'vertical',
66
+ extendSafeArea: false
67
+ })
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
+
74
+ const content = Ti.UI.createScrollView({
75
+ top: 0, left: 0, right: 0, bottom: 0,
76
+ layout: 'vertical', contentHeight: Ti.UI.SIZE,
77
+ scrollType: 'vertical', showVerticalScrollIndicator: true
78
+ })
79
+ const page = Ti.UI.createView({
80
+ width: Ti.UI.FILL, height: Ti.UI.SIZE, layout: 'vertical', bottom: '32dp'
81
+ })
82
+ content.add(page)
83
+
84
+ const bannerHost = Ti.UI.createView({
85
+ left: 0, right: 0, bottom: 0, height: 0, backgroundColor: colors.surface
86
+ })
87
+
88
+ const masthead = Ti.UI.createView({
89
+ top: '24dp', left: '24dp', right: '24dp', height: Ti.UI.SIZE, layout: 'vertical'
90
+ })
91
+ masthead.add(Ti.UI.createLabel({
92
+ text: 'TITANIUM / ADMOB', left: 0, height: Ti.UI.SIZE,
93
+ color: colors.accent, font: { fontSize: '12dp', fontWeight: 'bold' }
94
+ }))
95
+ masthead.add(Ti.UI.createLabel({
96
+ text: 'Ad playground.', top: '8dp', left: 0, right: 0, height: Ti.UI.SIZE,
97
+ color: colors.ink, font: { fontSize: '36dp', fontWeight: 'bold' }
98
+ }))
99
+ masthead.add(Ti.UI.createLabel({
100
+ text: 'Explore the formats. See the experience.',
101
+ top: '8dp', left: 0, right: 0, height: Ti.UI.SIZE,
102
+ color: colors.muted, font: { fontSize: '16dp' }
103
+ }))
104
+ page.add(masthead)
105
+
106
+ const session = Ti.UI.createView({
107
+ top: '12dp', bottom: '12dp', left: '24dp', right: '24dp', height: Ti.UI.SIZE,
108
+ layout: 'vertical', backgroundColor: colors.tint, borderRadius: '12dp'
109
+ })
110
+ const sessionLabel = Ti.UI.createLabel({
111
+ text: 'SETUP / PRIVACY', top: '16dp', left: '16dp', right: '16dp',
112
+ height: Ti.UI.SIZE, color: colors.accent,
113
+ font: { fontSize: '12dp', fontWeight: 'bold' }
114
+ })
115
+ const statusLabel = Ti.UI.createLabel({
116
+ text: 'Requesting privacy information…',
117
+ top: '8dp', left: '16dp', right: '16dp', bottom: '16dp',
118
+ height: Ti.UI.SIZE, verticalAlign: Ti.UI.TEXT_VERTICAL_ALIGNMENT_TOP, color: colors.ink,
119
+ font: { fontSize: '16dp', fontWeight: 'semibold' }
120
+ })
121
+ session.add(sessionLabel)
122
+ session.add(statusLabel)
123
+
124
+ const activity = Ti.UI.createLabel({
125
+ text: 'Session events will appear here.', left: '16dp', right: '16dp',
126
+ top: '16dp', bottom: '16dp', height: Ti.UI.SIZE,
127
+ color: colors.muted, font: { fontSize: '12dp', fontFamily: isAndroid ? 'monospace' : 'Menlo' }
128
+ })
129
+ const events = []
130
+ function setStatus(message) {
131
+ statusLabel.text = message
132
+ const now = new Date()
133
+ const time = [now.getHours(), now.getMinutes(), now.getSeconds()]
134
+ .map(value => String(value).padStart(2, '0')).join(':')
135
+ events.push(`${time} ${message}`)
136
+ if (events.length > 8) events.shift()
137
+ activity.text = events.join('\n\n')
138
+ Ti.API.info(`[ti.admob.unified example] ${message}`)
139
+ }
140
+
141
+ function setBannerHeight(value) {
142
+ const height = Math.max(0, Number(value) || 0)
143
+ const dimension = `${height}dp`
144
+ if (bannerHost.height !== dimension) bannerHost.height = dimension
145
+ if (content.bottom !== dimension) content.bottom = dimension
146
+ }
147
+
148
+ let currentSection
149
+ const formatCards = []
150
+ const fullscreenButtons = []
151
+ function cancelPendingFullscreenShows() {
152
+ fullscreenButtons.forEach(button => button.cancelPendingShow())
153
+ }
154
+ function createSection(title, description, grid) {
155
+ page.add(Ti.UI.createLabel({
156
+ text: title, top: '32dp', left: '24dp', right: '24dp', height: Ti.UI.SIZE,
157
+ color: colors.ink, font: { fontSize: '24dp', fontWeight: 'bold' }
158
+ }))
159
+ page.add(Ti.UI.createLabel({
160
+ text: description, top: '6dp', left: '24dp', right: '24dp', height: Ti.UI.SIZE,
161
+ color: colors.muted, font: { fontSize: '14dp' }
162
+ }))
163
+ currentSection = Ti.UI.createView({
164
+ left: '16dp', right: '16dp', height: Ti.UI.SIZE,
165
+ layout: grid ? 'horizontal' : 'vertical', horizontalWrap: true
166
+ })
167
+ page.add(currentSection)
168
+ }
169
+
170
+ // Fullscreen controls only present ready ads; loading starts automatically.
171
+ // Banner controls select the placement in the single shared slot.
172
+ function createAdButton(title, handlers) {
173
+ const isFormat = Boolean(handlers.description)
174
+ const fullscreen = Boolean(handlers.onShow)
175
+ const idleTitle = fullscreen ? `Show ${title.toLowerCase()}` : isFormat ? `Use ${title.toLowerCase()}` : title
176
+ const card = isFormat ? Ti.UI.createView({
177
+ left: '8dp', right: '8dp', top: '16dp', width: '280dp',
178
+ height: Ti.UI.SIZE, layout: 'vertical', backgroundColor: colors.surface,
179
+ borderRadius: '12dp', borderColor: colors.line, borderWidth: 1
180
+ }) : null
181
+ if (card) {
182
+ card.add(Ti.UI.createLabel({
183
+ text: title, top: '20dp', left: '20dp', right: '20dp', height: Ti.UI.SIZE,
184
+ color: colors.ink, font: { fontSize: '20dp', fontWeight: 'bold' }
185
+ }))
186
+ card.add(Ti.UI.createLabel({
187
+ text: handlers.description, top: '8dp', left: '20dp', right: '20dp',
188
+ height: Ti.UI.SIZE, color: colors.muted, font: { fontSize: '14dp' }
189
+ }))
190
+ currentSection.add(card)
191
+ formatCards.push(card)
192
+ }
193
+ const button = Ti.UI.createButton({
194
+ title: isFormat ? 'Waiting for initialization…' : idleTitle,
195
+ enabled: !isFormat,
196
+ top: isFormat ? '12dp' : '8dp', left: isFormat ? '20dp' : '8dp',
197
+ right: isFormat ? '20dp' : '8dp', bottom: isFormat ? '20dp' : 0,
198
+ height: '48dp', color: colors.accent, backgroundColor: colors.tint,
199
+ backgroundSelectedColor: colors.line, borderRadius: '8dp',
200
+ font: { fontSize: '14dp', fontWeight: 'semibold' },
201
+ accessibilityHint: handlers.description || ''
202
+ })
203
+ let state = 'idle'
204
+ let pendingShow = false
205
+ let introPending = false
206
+ let introAccepted = false
207
+ let showIntent = 0
208
+ const sizeGuard = fullscreen ? createFullscreenSizeGuard(() => win) : null
209
+ button.creating = function () {
210
+ state = 'creating'
211
+ button.applyProperties({ title: 'Loading…', backgroundColor: colors.quiet, color: colors.muted, enabled: false })
212
+ }
213
+ button.ready = function () {
214
+ state = 'ready'
215
+ button.applyProperties({
216
+ title: `Show ${title.toLowerCase()}`, backgroundColor: colors.accent,
217
+ backgroundSelectedColor: colors.pressed, color: colors.surface, enabled: true
218
+ })
219
+ if (pendingShow) presentRequestedAd()
220
+ }
221
+ button.cancelPendingShow = function () {
222
+ pendingShow = false
223
+ introPending = false
224
+ introAccepted = false
225
+ showIntent += 1
226
+ }
227
+ button.reset = function () {
228
+ button.cancelPendingShow()
229
+ state = 'idle'
230
+ button.applyProperties({
231
+ title: isFormat && consentBusy ? 'Waiting for consent…'
232
+ : isFormat && !initializationComplete ? 'Waiting for initialization…'
233
+ : fullscreen ? 'Waiting to load…' : idleTitle,
234
+ backgroundColor: colors.tint, backgroundSelectedColor: colors.line,
235
+ color: colors.accent, enabled: !fullscreen && (!isFormat || (initializationComplete && !consentBusy && Admob.canRequestAds()))
236
+ })
237
+ }
238
+ button.failed = function () {
239
+ button.reset()
240
+ state = 'failed'
241
+ button.applyProperties({ title: `Retry ${title.toLowerCase()}`, enabled: true })
242
+ }
243
+ button.loaded = function () {
244
+ state = 'loaded'
245
+ button.applyProperties({
246
+ title: `Reload ${title.toLowerCase()}`, backgroundColor: colors.tint,
247
+ backgroundSelectedColor: colors.line, color: colors.accent, enabled: true
248
+ })
249
+ }
250
+ button.load = function () {
251
+ if (windowClosed || consentBusy || state === 'creating' || state === 'showing') return
252
+ if (!handlers.privacy && (!initializationComplete || !Admob.canRequestAds())) {
253
+ setStatus(`${title} blocked until privacy and initialization complete`)
254
+ return
255
+ }
256
+ button.creating()
257
+ if (sizeGuard) sizeGuard.recordLoad()
258
+ handlers.onLoad(button)
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
+ }
286
+ button.addEventListener('click', () => {
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`)
291
+ return
292
+ }
293
+ cancelPendingFullscreenShows()
294
+ pendingShow = true
295
+ if (state === 'ready') presentRequestedAd()
296
+ else button.load()
297
+ return
298
+ }
299
+ button.load()
300
+ })
301
+ if (fullscreen) fullscreenButtons.push(button)
302
+ const parent = card || currentSection
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
+ }
313
+ return button
314
+ }
315
+
316
+ // Classic Titanium has no TSS breakpoints. Measure the available viewport,
317
+ // not the physical display, so rotation and iPad split view use the same layout.
318
+ let previousWidth = 0
319
+ function layoutPage() {
320
+ const width = Ti.UI.convertUnits(content.size.width, Ti.UI.UNIT_DIP)
321
+ if (!width || width === previousWidth) return
322
+ previousWidth = width
323
+ const pageWidth = Math.min(width, 960)
324
+ page.width = `${pageWidth}dp`
325
+ const columns = pageWidth >= 700 ? 2 : 1
326
+ const cardWidth = Math.floor((pageWidth - 32) / columns) - 16
327
+ formatCards.forEach(card => { card.width = `${cardWidth}dp` })
328
+ }
329
+ content.addEventListener('postlayout', layoutPage)
330
+
331
+ function initializeAdsOnce() {
332
+ if (
333
+ windowClosed ||
334
+ initializationStarted ||
335
+ !Admob.canRequestAds()
336
+ ) {
337
+ return
338
+ }
339
+
340
+ initializationStarted = true
341
+ setStatus('Initializing Google Mobile Ads…')
342
+
343
+ Admob.initialize({
344
+ ageRestrictedTreatment: Admob.AGE_RESTRICTED_TREATMENT_UNSPECIFIED,
345
+ maxAdContentRating: Admob.MAX_AD_CONTENT_RATING_GENERAL
346
+ })
347
+ }
348
+
349
+ function onModuleInitialized(event) {
350
+ if (windowClosed || initializationComplete) return
351
+ if (!event.success) {
352
+ setStatus(`Initialization failed: ${event.error}`)
353
+ return
354
+ }
355
+
356
+ Ti.API.info('Google adapter initialization: ' + JSON.stringify(Admob.getMediationStatus()))
357
+ initializationComplete = true
358
+ updateConsentTools()
359
+ sessionLabel.text = 'TEST ADS / SDK READY'
360
+ loadExampleAds()
361
+ }
362
+
363
+ function loadExampleAds() {
364
+ setStatus('Preparing ads automatically…')
365
+ interstitialButton.load()
366
+ rewardedButton.load()
367
+ appOpenButton.load()
368
+ adaptiveButton.load()
369
+ rewardedInterstitialButton.load()
370
+ adsStoppedForConsent = false
371
+ preserveConsentBannerSlot = false
372
+ }
373
+
374
+ function onTrackingAuthorizationCompleted(event) {
375
+ trackingRequestStarted = false
376
+ Admob.removeEventListener(
377
+ 'tracking_authorization_completed',
378
+ onTrackingAuthorizationCompleted
379
+ )
380
+ Ti.API.info(`ATT status: ${event.status}`)
381
+ Ti.API.info('Current ATT state: ' + JSON.stringify(Admob.getTrackingAuthorizationStatus()))
382
+ initializeAdsOnce()
383
+ updateConsentTools()
384
+ }
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
+
416
+ function continueAfterPrivacyGate(canRequestAds) {
417
+ if (!canRequestAds || initializationStarted || windowClosed) {
418
+ return
419
+ }
420
+
421
+ if (trackingRequestStarted) {
422
+ return
423
+ }
424
+
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()
442
+ }
443
+
444
+ // Reopening is separate from initial consent; unknown/error is not NOT_REQUIRED.
445
+ function setPrivacyOptionsAvailable(available, error) {
446
+ const required = available === true || available === 1
447
+ privacyNote.text = error ? 'Privacy update failed. See Session activity for details.'
448
+ : available === false || available === 0 ? 'No privacy form is required for this session.'
449
+ : 'Checking whether privacy options are required…'
450
+ privacyButton.applyProperties({
451
+ visible: required, touchEnabled: required,
452
+ height: '48dp', top: '8dp'
453
+ })
454
+ privacyNote.applyProperties({
455
+ visible: !required, height: '48dp', top: '8dp'
456
+ })
457
+ }
458
+
459
+ function updateConsentTools() {
460
+ const busy = consentBusy || trackingRequestStarted || (initializationStarted && !initializationComplete)
461
+ consentTestButtons.forEach(button => {
462
+ button.enabled = !busy
463
+ button.title = busy ? 'Run test — please wait…' : 'Run test'
464
+ })
465
+ updateTrackingStatus()
466
+ }
467
+
468
+ function stopAdsForConsent() {
469
+ preserveConsentBannerSlot = true
470
+ destroyBanner()
471
+ destroyInterstitial()
472
+ destroyRewarded()
473
+ destroyRewardedInterstitial()
474
+ destroyAppOpen()
475
+ interstitialButton.reset()
476
+ rewardedButton.reset()
477
+ rewardedInterstitialButton.reset()
478
+ appOpenButton.reset()
479
+ adsStoppedForConsent = true
480
+ }
481
+
482
+ function requestExampleConsent(mode, reset = false) {
483
+ if (windowClosed || consentBusy || trackingRequestStarted || (initializationStarted && !initializationComplete)) return
484
+ if (!consentTestingEnabled && (reset || mode !== 'normal')) return
485
+ let deviceIds = []
486
+ if (consentTestingEnabled) {
487
+ const value = consentDeviceInput.value.trim()
488
+ deviceIds = value.split(/[\s,]+/).filter(Boolean)
489
+ const deviceIdPattern = isAndroid ? /^[a-f0-9]{32}$/i : /^(?:[a-f0-9]{32}|[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12})$/i
490
+ if (mode !== 'normal' && deviceIds.some(id => !deviceIdPattern.test(id))) {
491
+ consentDemoFeedback.text = 'Paste the UMP test-device identifier exactly as logged by the SDK, including any hyphens. Separate devices with commas.'
492
+ setStatus(consentDemoFeedback.text)
493
+ return
494
+ }
495
+ if (mode !== 'normal' && isAndroid && !deviceIds.length) {
496
+ consentDemoFeedback.text = 'Paste this phone’s UMP test-device hash above, then tap Run test. Enter alone does not start the test.'
497
+ setStatus(consentDemoFeedback.text)
498
+ return
499
+ }
500
+ consentDeviceInput.blur()
501
+ Ti.App.Properties.setString('demo.consentDeviceIds', value)
502
+ Ti.App.Properties.setString('demo.consentMode', mode)
503
+ }
504
+ if (consentDemoFeedback) consentDemoFeedback.text = `Requesting ${consentModes[mode].label}… Please wait for Google’s response.`
505
+ consentMode = mode
506
+ openUSOptionsAfterRequest = reset && mode === 'us'
507
+ consentBusy = true
508
+ if (reset) {
509
+ stopAdsForConsent()
510
+ Admob.resetConsent()
511
+ }
512
+ setPrivacyOptionsAvailable(undefined)
513
+ updateConsentTools()
514
+ setStatus(`Requesting consent — ${consentModes[mode].label}${reset ? ' (test consent reset)' : ''}`)
515
+ const config = mode === 'normal' ? {} : { geography: consentModes[mode].geography, testDeviceIds: deviceIds }
516
+ Admob.requestConsent(config)
517
+ }
518
+
519
+ function finishExampleConsent(event, source) {
520
+ const requiresOptions = event.privacyOptionsRequired === true || event.privacyOptionsRequired === 1
521
+ if (openUSOptionsAfterRequest && source === 'Consent completed') {
522
+ openUSOptionsAfterRequest = false
523
+ if (event.success && requiresOptions) {
524
+ if (consentDemoFeedback) consentDemoFeedback.text = 'Opening the US privacy form… Choose your preference, then save and close.'
525
+ setPrivacyOptionsAvailable(event.privacyOptionsRequired)
526
+ Admob.presentPrivacyOptions({})
527
+ return
528
+ }
529
+ }
530
+ consentBusy = false
531
+ if (consentDemoFeedback) {
532
+ const result = !event.success ? 'Request failed. See Session activity; tap Run test to retry.'
533
+ : source === 'Privacy options closed' ? 'Privacy form closed. Your choice has been processed.'
534
+ : requiresOptions ? 'Completed. Review your choice in Advertising privacy options.'
535
+ : consentMode === 'normal' ? 'Actual region restored. Google reports no privacy form is needed.'
536
+ : 'Completed. No privacy form is required.'
537
+ consentDemoFeedback.text = `${consentModes[consentMode].label}: ${result}` + (event.canRequestAds ? ' Ads may load.' : ' Ads remain stopped.')
538
+ }
539
+ if (!event.canRequestAds) {
540
+ preserveConsentBannerSlot = false
541
+ setBannerHeight(0)
542
+ }
543
+ setPrivacyOptionsAvailable(event.privacyOptionsRequired, event.success ? null : event.error || 'Unknown error')
544
+ setStatus(`${source}: success=${event.success}; canRequestAds=${event.canRequestAds}; privacyOptionsRequired=${event.privacyOptionsRequired}${event.error ? `; error=${event.error}` : ''}`)
545
+ if (event.canRequestAds) {
546
+ if (initializationComplete && adsStoppedForConsent) loadExampleAds()
547
+ else continueAfterPrivacyGate(true)
548
+ }
549
+ updateConsentTools()
550
+ }
551
+
552
+ function onConsentComplete(event) {
553
+ finishExampleConsent(event, 'Consent completed')
554
+ }
555
+
556
+ function onPrivacyOptionsDismissed(event) {
557
+ finishExampleConsent(event, 'Privacy options closed')
558
+ }
559
+
560
+ function onAdInspectorClosed(event) {
561
+ if (!event.success) {
562
+ setStatus(`Ad Inspector failed: ${event.error} (${event.code})`)
563
+ return
564
+ }
565
+ setStatus('Ad Inspector closed')
566
+ }
567
+
568
+ // ---------------------------------------------------------------------------
569
+ // Banners: one container, reused. Selecting a banner throws away whatever
570
+ // is in the host and builds the requested type in its place.
571
+ // ---------------------------------------------------------------------------
572
+
573
+ // platformWidth is in pixels on Android and in points on iOS, and points are
574
+ // already the unit customAdSize expects. Dividing on both platforms is the
575
+ // mistake that looks right: on a 3x iPhone it asks for a third of the screen.
576
+ function screenWidthInDp() {
577
+ const hostWidth = Ti.UI.convertUnits(content.size.width, Ti.UI.UNIT_DIP)
578
+ if (hostWidth > 0) return Math.round(hostWidth)
579
+ const caps = Ti.Platform.displayCaps
580
+
581
+ if (Ti.Platform.osname === 'android') {
582
+ return Math.round(caps.platformWidth / caps.logicalDensityFactor)
583
+ }
584
+
585
+ return Math.round(caps.platformWidth)
586
+ }
587
+
588
+ function onBannerLoaded(event) {
589
+ setBannerHeight(event.height)
590
+ let message = 'Loaded in the banner slot'
591
+ if (activeBannerButton === collapsibleButton) {
592
+ message = event.isCollapsible === true
593
+ ? 'Collapsible ad received. Its arrow controls collapse.'
594
+ : event.isCollapsible === false
595
+ ? 'Google returned a standard banner. No collapse arrow.'
596
+ : 'Banner loaded. Collapse capability was not reported.'
597
+ }
598
+ setStatus(`Adaptive banner loaded at ${event.height}${event.heightUnit}; isCollapsible=${event.isCollapsible}. ${message}`)
599
+ activeBannerButton.loaded()
600
+ }
601
+
602
+ function onFixedBannerLoaded() {
603
+ setStatus(`Fixed banner loaded at ${screenWidthInDp()}x50dp`)
604
+ fixedButton.loaded()
605
+ }
606
+
607
+ function onBannerFailed(event) {
608
+ setBannerHeight(0)
609
+ setStatus(`Banner failed: ${event.error} (${event.code})`)
610
+ if (activeBannerButton) activeBannerButton.failed()
611
+ }
612
+
613
+ function onBannerDestroyed() {
614
+ if (activeBannerButton) activeBannerButton.reset()
615
+ setBannerHeight(0)
616
+ }
617
+
618
+ function destroyBanner(preserveSlot = false) {
619
+ fixedButton.reset()
620
+ adaptiveButton.reset()
621
+ collapsibleButton.reset()
622
+ activeBannerButton = null
623
+ if (!banner) {
624
+ if (!preserveSlot && !preserveConsentBannerSlot) setBannerHeight(0)
625
+ return
626
+ }
627
+
628
+ banner.removeEventListener('postlayout', onAdaptiveLayout)
629
+ banner.removeEventListener(Admob.AD_LOADED, onBannerLoaded)
630
+ banner.removeEventListener(Admob.AD_LOADED, onFixedBannerLoaded)
631
+ banner.removeEventListener(Admob.AD_FAILED_TO_LOAD, onBannerFailed)
632
+ banner.removeEventListener(Admob.AD_DESTROYED, onBannerDestroyed)
633
+ banner.destroy()
634
+
635
+ try {
636
+ bannerHost.remove(banner)
637
+ } catch (error) {
638
+ Ti.API.debug(`Banner was already detached: ${error.message}`)
639
+ }
640
+
641
+ banner = null
642
+ if (!preserveSlot && !preserveConsentBannerSlot) setBannerHeight(0)
643
+ }
644
+
645
+ // A fixed slot as wide as the screen and 50dp tall. Both numbers come from the
646
+ // application: the module passes them to AdSize(width, height) on Android and
647
+ // GADAdSizeFromCGSize on iOS, and neither platform adjusts them. Because the
648
+ // height is known up front, the slot is reserved before the ad arrives and
649
+ // nothing on screen moves when it loads.
650
+ function loadFixedFullWidthBanner() {
651
+ if (banner && activeBannerButton === fixedButton) {
652
+ banner.load()
653
+ return
654
+ }
655
+ destroyBanner(true)
656
+ activeBannerButton = fixedButton
657
+ fixedButton.creating()
658
+
659
+ banner = Admob.createBanner({
660
+ adUnitId: Admob.TEST_BANNER,
661
+ customAdSize: { width: screenWidthInDp(), height: 50 },
662
+ requestAgent: 'ti.admob.unified example',
663
+ width: Ti.UI.FILL
664
+ })
665
+
666
+ banner.addEventListener(Admob.AD_LOADED, onFixedBannerLoaded)
667
+ banner.addEventListener(Admob.AD_FAILED_TO_LOAD, onBannerFailed)
668
+ banner.addEventListener(Admob.AD_DESTROYED, onBannerDestroyed)
669
+
670
+ bannerHost.add(banner)
671
+ setBannerHeight(50)
672
+ banner.load()
673
+ }
674
+
675
+ // Anchored adaptive: the application gives the width, Google picks the height.
676
+ // Size the iOS placement using Google before loading, then wait for layout.
677
+ // The expanded creative overlays the app; its height is not reserved here.
678
+ function onAdaptiveLayout() {
679
+ if (!banner || banner.rect.width <= 0 || banner.rect.height <= 0) return
680
+ banner.removeEventListener('postlayout', onAdaptiveLayout)
681
+ banner.load()
682
+ }
683
+
684
+ function loadAdaptiveBanner(collapsible = false) {
685
+ const selectedButton = collapsible ? collapsibleButton : adaptiveButton
686
+ if (banner && activeBannerButton === selectedButton) {
687
+ if (isAndroid || (banner.rect.width > 0 && banner.rect.height > 0)) banner.load()
688
+ else banner.addEventListener('postlayout', onAdaptiveLayout)
689
+ return
690
+ }
691
+ destroyBanner(true)
692
+ activeBannerButton = selectedButton
693
+ activeBannerButton.creating()
694
+
695
+ banner = Admob.createAdaptiveBanner({
696
+ adUnitId: Admob.TEST_ADAPTIVE_BANNER,
697
+ adaptiveType: Admob.ADAPTIVE_ANCHORED,
698
+ ...(collapsible ? { collapsible: 'bottom' } : {}),
699
+ ...(!isAndroid ? { height: Ti.UI.FILL } : {}),
700
+ requestAgent: 'ti.admob.unified example',
701
+ width: Ti.UI.FILL
702
+ })
703
+
704
+ banner.addEventListener(Admob.AD_LOADED, onBannerLoaded)
705
+ banner.addEventListener(Admob.AD_FAILED_TO_LOAD, onBannerFailed)
706
+ banner.addEventListener(Admob.AD_DESTROYED, onBannerDestroyed)
707
+
708
+ if (isAndroid) {
709
+ bannerHost.add(banner)
710
+ banner.load()
711
+ } else {
712
+ setBannerHeight(Admob.getAnchoredAdaptiveBannerSize(bannerHost.rect.width || screenWidthInDp()).height)
713
+ banner.addEventListener('postlayout', onAdaptiveLayout)
714
+ bannerHost.add(banner)
715
+ }
716
+ }
717
+
718
+ // ---------------------------------------------------------------------------
719
+ // Fullscreen formats preload automatically. Only presentation needs a tap.
720
+ // A consumed ad is replaced after close, without automatically showing it.
721
+ // ---------------------------------------------------------------------------
722
+
723
+ function destroyInterstitial() {
724
+ if (!interstitial) {
725
+ return
726
+ }
727
+
728
+ interstitial.removeEventListener(Admob.AD_LOADED, onInterstitialLoaded)
729
+ interstitial.removeEventListener(Admob.AD_FAILED_TO_LOAD, onInterstitialFailed)
730
+ interstitial.removeEventListener(Admob.AD_FAILED_TO_SHOW, onInterstitialFailed)
731
+ interstitial.removeEventListener(Admob.AD_CLOSED, onInterstitialClosed)
732
+ interstitial.destroy()
733
+ interstitial = null
734
+ }
735
+
736
+ function onInterstitialLoaded() {
737
+ setStatus('Interstitial ready to show')
738
+ interstitialButton.ready()
739
+ }
740
+
741
+ function onInterstitialFailed(event) {
742
+ setStatus(`Interstitial failed: ${event.error} (${event.code})`)
743
+ interstitialButton.failed()
744
+ }
745
+
746
+ function onInterstitialClosed() {
747
+ setStatus('Interstitial closed')
748
+ interstitialButton.reset()
749
+ interstitialButton.load()
750
+ }
751
+
752
+ function destroyRewarded() {
753
+ if (!rewarded) {
754
+ return
755
+ }
756
+
757
+ rewarded.removeEventListener(Admob.AD_LOADED, onRewardedLoaded)
758
+ rewarded.removeEventListener(Admob.AD_FAILED_TO_LOAD, onRewardedFailed)
759
+ rewarded.removeEventListener(Admob.AD_FAILED_TO_SHOW, onRewardedFailed)
760
+ rewarded.removeEventListener(Admob.AD_REWARDED, onRewardEarned)
761
+ rewarded.removeEventListener(Admob.AD_CLOSED, onRewardedClosed)
762
+ rewarded.destroy()
763
+ rewarded = null
764
+ }
765
+
766
+ function onRewardedLoaded() {
767
+ setStatus('Rewarded ad ready to show')
768
+ rewardedButton.ready()
769
+ }
770
+
771
+ function onRewardedFailed(event) {
772
+ setStatus(`Rewarded ad failed: ${event.error} (${event.code})`)
773
+ rewardedButton.failed()
774
+ }
775
+
776
+ // Closing a rewarded ad is not proof the reward was earned. Only this event is.
777
+ function onRewardEarned(event) {
778
+ setStatus(`Reward earned: ${event.rewardAmount} ${event.rewardType}`)
779
+ }
780
+
781
+ function onRewardedClosed() {
782
+ rewardedButton.reset()
783
+ rewardedButton.load()
784
+ }
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
+
812
+ function destroyAppOpen() {
813
+ if (!appOpen) {
814
+ return
815
+ }
816
+
817
+ appOpen.removeEventListener(Admob.AD_LOADED, onAppOpenLoaded)
818
+ appOpen.removeEventListener(Admob.AD_FAILED_TO_LOAD, onAppOpenFailed)
819
+ appOpen.removeEventListener(Admob.AD_FAILED_TO_SHOW, onAppOpenFailedToShow)
820
+ appOpen.removeEventListener(Admob.AD_CLOSED, onAppOpenClosed)
821
+ appOpen.destroy()
822
+ appOpen = null
823
+ }
824
+
825
+ function onAppOpenLoaded() {
826
+ setStatus('App open ad ready to show')
827
+ appOpenButton.ready()
828
+ }
829
+
830
+ function onAppOpenFailed(event) {
831
+ setStatus(`App open ad failed: ${event.error} (${event.code})`)
832
+ appOpenButton.failed()
833
+ }
834
+
835
+ // An app open ad expires four hours after the request. Past that point the
836
+ // proxy answers show() with this event instead of presenting an ad that would
837
+ // no longer earn revenue.
838
+ function onAppOpenFailedToShow(event) {
839
+ setStatus(`App open ad could not show: ${event.error} (${event.code})`)
840
+ appOpenButton.failed()
841
+ }
842
+
843
+ function onAppOpenClosed() {
844
+ setStatus('App open ad closed')
845
+ appOpenButton.reset()
846
+ appOpenButton.load()
847
+ }
848
+
849
+ // The module owns application/activity backgrounding. The app calls these only
850
+ // when its own navigation stops showing this placement, such as an inactive tab
851
+ // or a child window covering it. Neither call changes visibility or layout.
852
+ function pauseBannerForCoveredScreen() {
853
+ if (banner) {
854
+ banner.pause()
855
+ }
856
+ }
857
+
858
+ function resumeBannerForVisibleScreen() {
859
+ if (banner) {
860
+ banner.resume()
861
+ }
862
+ }
863
+
864
+ // ---------------------------------------------------------------------------
865
+ // Screen
866
+ // ---------------------------------------------------------------------------
867
+
868
+ createSection('Fullscreen ads', 'Ads load automatically. Tap Show when you’re ready.', true)
869
+
870
+ const interstitialButton = createAdButton('Interstitial', {
871
+ description: 'A full-screen break between moments in your app.',
872
+ onLoad: () => {
873
+ destroyInterstitial()
874
+ interstitial = Admob.createInterstitial({
875
+ adUnitId: Admob.TEST_INTERSTITIAL,
876
+ requestAgent: 'ti.admob.unified example',
877
+ autoReload: false
878
+ })
879
+
880
+ interstitial.addEventListener(Admob.AD_LOADED, onInterstitialLoaded)
881
+ interstitial.addEventListener(Admob.AD_FAILED_TO_LOAD, onInterstitialFailed)
882
+ interstitial.addEventListener(Admob.AD_FAILED_TO_SHOW, onInterstitialFailed)
883
+ interstitial.addEventListener(Admob.AD_CLOSED, onInterstitialClosed)
884
+ interstitial.load()
885
+ },
886
+ onShow: () => interstitial.show()
887
+ })
888
+
889
+ const rewardedButton = createAdButton('Rewarded', {
890
+ description: 'An opt-in experience that earns an in-app reward.',
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
+ }
897
+ destroyRewarded()
898
+ rewarded = Admob.createRewarded({
899
+ adUnitId: Admob.TEST_REWARDED,
900
+ requestAgent: 'ti.admob.unified example',
901
+ autoReload: false
902
+ })
903
+
904
+ rewarded.addEventListener(Admob.AD_LOADED, onRewardedLoaded)
905
+ rewarded.addEventListener(Admob.AD_FAILED_TO_LOAD, onRewardedFailed)
906
+ rewarded.addEventListener(Admob.AD_FAILED_TO_SHOW, onRewardedFailed)
907
+ rewarded.addEventListener(Admob.AD_REWARDED, onRewardEarned)
908
+ rewarded.addEventListener(Admob.AD_CLOSED, onRewardedClosed)
909
+ rewarded.load()
910
+ },
911
+ onShow: () => rewarded.show()
912
+ })
913
+
914
+ const appOpenButton = createAdButton('App open', {
915
+ description: 'A full-screen placement for an app’s opening moment.',
916
+ onLoad: () => {
917
+ destroyAppOpen()
918
+ appOpen = Admob.createAppOpen({
919
+ adUnitId: Admob.TEST_APP_OPEN,
920
+ requestAgent: 'ti.admob.unified example',
921
+ autoReload: false
922
+ })
923
+
924
+ appOpen.addEventListener(Admob.AD_LOADED, onAppOpenLoaded)
925
+ appOpen.addEventListener(Admob.AD_FAILED_TO_LOAD, onAppOpenFailed)
926
+ appOpen.addEventListener(Admob.AD_FAILED_TO_SHOW, onAppOpenFailedToShow)
927
+ appOpen.addEventListener(Admob.AD_CLOSED, onAppOpenClosed)
928
+ appOpen.load()
929
+ },
930
+ onShow: () => appOpen.show()
931
+ })
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
+
970
+ createSection('Banner ads', 'Adaptive starts automatically. Choose a format for the banner slot.', true)
971
+
972
+ const fixedButton = createAdButton('Fixed banner', {
973
+ description: 'A full-width placement with a fixed 50 dp height.',
974
+ onLoad: () => loadFixedFullWidthBanner()
975
+ })
976
+
977
+ const adaptiveButton = createAdButton('Adaptive banner', {
978
+ description: 'Fits the available width. Google selects the height.',
979
+ onLoad: () => loadAdaptiveBanner()
980
+ })
981
+
982
+ const collapsibleButton = createAdButton('Collapsible banner', {
983
+ description: 'Loads a new ad requesting expansion. Google may return a standard banner.',
984
+ onLoad: () => loadAdaptiveBanner(true)
985
+ })
986
+
987
+ createSection('Banner controls', 'Manage the current placement without creating a new ad.', false)
988
+
989
+ createAdButton('Remove banner', {
990
+ onLoad: button => {
991
+ destroyBanner()
992
+ setStatus('Banner destroyed and host collapsed')
993
+ button.reset()
994
+ }
995
+ })
996
+
997
+ createAdButton('Pause banner', {
998
+ onLoad: button => {
999
+ pauseBannerForCoveredScreen()
1000
+ setStatus(banner ? 'Banner paused without hiding or changing its slot' : 'Load a banner first to pause it')
1001
+ button.reset()
1002
+ }
1003
+ })
1004
+
1005
+ createAdButton('Resume banner', {
1006
+ onLoad: button => {
1007
+ resumeBannerForVisibleScreen()
1008
+ setStatus(banner ? 'The same banner instance resumed' : 'Load a banner first to resume it')
1009
+ button.reset()
1010
+ }
1011
+ })
1012
+
1013
+ createSection('Privacy & consent', 'Your choices are managed by Google’s consent form.', false)
1014
+ // Button and status share one stable slot instead of changing the page height.
1015
+ currentSection.layout = 'composite'
1016
+ currentSection.height = '64dp'
1017
+
1018
+ const privacyButton = createAdButton('Advertising privacy options', {
1019
+ privacy: true,
1020
+ onLoad: button => {
1021
+ consentBusy = true
1022
+ stopAdsForConsent()
1023
+ updateConsentTools()
1024
+ Admob.presentPrivacyOptions({})
1025
+ button.reset()
1026
+ }
1027
+ })
1028
+
1029
+ const privacyNote = Ti.UI.createLabel({
1030
+ top: '8dp',
1031
+ left: '8dp',
1032
+ right: '8dp',
1033
+ height: Ti.UI.SIZE,
1034
+ color: colors.muted,
1035
+ font: { fontSize: 13 },
1036
+ text: 'UMP reports no privacy form for this session. It appears in the EEA, '
1037
+ + 'and wherever a message is configured for the app in AdMob.'
1038
+ })
1039
+ currentSection.add(privacyNote)
1040
+
1041
+ setPrivacyOptionsAvailable(undefined)
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
+
1067
+ if (consentTestingEnabled) {
1068
+ createSection('Consent demo', 'Choose a scenario and run it. Testing clears the previous consent choice.', false)
1069
+ currentSection.add(Ti.UI.createLabel({
1070
+ text: 'Test device', top: '12dp', left: '8dp', right: '8dp', height: Ti.UI.SIZE,
1071
+ color: colors.ink, font: { fontSize: '14dp', fontWeight: 'bold' }
1072
+ }))
1073
+ currentSection.add(Ti.UI.createLabel({
1074
+ text: 'Paste the UMP test-device identifier from the SDK log, including any hyphens. This app remembers it. Leave empty on an iOS simulator.',
1075
+ top: '4dp', left: '8dp', right: '8dp', height: Ti.UI.SIZE,
1076
+ color: colors.muted, font: { fontSize: '13dp' }
1077
+ }))
1078
+ consentDeviceInput = Ti.UI.createTextField({
1079
+ value: Ti.App.Properties.getString('demo.consentDeviceIds', '') || Ti.App.Properties.getString(`demo.consentDeviceIds.${isAndroid ? 'android' : 'ios'}`, ''), hintText: 'UMP test-device identifier (from SDK log)',
1080
+ top: '8dp', left: '8dp', right: '8dp', height: '48dp',
1081
+ color: colors.ink, backgroundColor: colors.surface,
1082
+ borderColor: colors.line, borderWidth: 1, borderRadius: '8dp',
1083
+ autocorrect: false, autocapitalization: Ti.UI.TEXT_AUTOCAPITALIZATION_NONE, font: { fontSize: '13dp' }
1084
+ })
1085
+ currentSection.add(consentDeviceInput)
1086
+ const scenarios = [
1087
+ ['eea', 'Europe — consent form', 'Shows the European consent flow.'],
1088
+ ['us', 'US — privacy options', 'Opens US privacy options when available.'],
1089
+ ['other', 'Other region — no form', 'Tests a region where no dialog is normally needed.'],
1090
+ ['normal', 'My actual region', 'Removes the override; Google decides whether a form is needed.']
1091
+ ]
1092
+ let selectedScenario = Math.max(0, scenarios.findIndex(item => item[0] === consentMode))
1093
+ const selector = Ti.UI.createButton({
1094
+ title: scenarios[selectedScenario][1] + ' ▾', enabled: true,
1095
+ top: '8dp', left: '8dp', right: '8dp', height: '48dp',
1096
+ color: colors.accent, backgroundColor: colors.tint, borderRadius: '8dp',
1097
+ font: { fontSize: '14dp', fontWeight: 'semibold' }
1098
+ })
1099
+ currentSection.add(selector)
1100
+ selector.addEventListener('click', () => {
1101
+ const dialog = Ti.UI.createOptionDialog({
1102
+ title: 'Privacy scenario', options: [...scenarios.map(item => item[1]), 'Cancel'], cancel: scenarios.length
1103
+ })
1104
+ dialog.addEventListener('click', event => {
1105
+ if (event.index < 0 || event.index >= scenarios.length) return
1106
+ selectedScenario = event.index
1107
+ selector.title = scenarios[selectedScenario][1] + ' ▾'
1108
+ scenarioHint.text = scenarios[selectedScenario][2]
1109
+ })
1110
+ dialog.show(isAndroid ? {} : { view: selector })
1111
+ })
1112
+ selector.title = scenarios[selectedScenario][1] + ' ▾'
1113
+ const scenarioHint = Ti.UI.createLabel({
1114
+ text: scenarios[selectedScenario][2], top: '8dp', left: '8dp', right: '8dp', height: Ti.UI.SIZE,
1115
+ color: colors.muted, font: { fontSize: '13dp' }
1116
+ })
1117
+ currentSection.add(scenarioHint)
1118
+ const runButton = createAdButton('Run test', {
1119
+ privacy: true,
1120
+ onLoad: button => { button.reset(); requestExampleConsent(scenarios[selectedScenario][0], true) }
1121
+ })
1122
+ consentTestButtons.push(runButton)
1123
+ consentDemoFeedback = Ti.UI.createLabel({
1124
+ text: 'Ready to test.', top: '8dp', left: '8dp', right: '8dp', height: Ti.UI.SIZE,
1125
+ color: colors.accent, font: { fontSize: '13dp' }
1126
+ })
1127
+ consentDemoFeedback.height = '64dp'
1128
+ consentDemoFeedback.verticalAlign = Ti.UI.TEXT_VERTICAL_ALIGNMENT_TOP
1129
+ currentSection.add(consentDemoFeedback)
1130
+ updateConsentTools()
1131
+ }
1132
+
1133
+ createSection('Developer tools', 'Inspect live ad requests with Google’s native Ad Inspector.', false)
1134
+
1135
+ createAdButton('Open Ad Inspector', {
1136
+ onLoad: button => {
1137
+ Admob.openAdInspector()
1138
+ button.reset()
1139
+ }
1140
+ })
1141
+
1142
+ createSection('Session activity', 'Only Google demo ad units are used.', false)
1143
+ const activityPanel = Ti.UI.createScrollView({
1144
+ top: '16dp', left: '8dp', right: '8dp', height: '240dp',
1145
+ contentHeight: Ti.UI.SIZE, scrollType: 'vertical', showVerticalScrollIndicator: true,
1146
+ backgroundColor: colors.surface, borderRadius: '12dp'
1147
+ })
1148
+ activityPanel.add(activity)
1149
+ currentSection.add(activityPanel)
1150
+ page.add(Ti.UI.createLabel({
1151
+ textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER,
1152
+ color: colors.muted, font: { fontSize: '12dp' },
1153
+ text: 'ti.admob.unified 1.0.0 / Google Mobile Ads + UMP',
1154
+ top: '24dp', left: '24dp', right: '24dp', height: Ti.UI.SIZE
1155
+ }))
1156
+
1157
+ Admob.addEventListener('module_initialized', onModuleInitialized)
1158
+ Admob.addEventListener('consent_complete', onConsentComplete)
1159
+ Admob.addEventListener(
1160
+ 'privacy_options_dismissed',
1161
+ onPrivacyOptionsDismissed
1162
+ )
1163
+ Admob.addEventListener('ad_inspector_closed', onAdInspectorClosed)
1164
+
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)
1171
+
1172
+ // On Android, Titanium also fires close while it recreates the activity, for
1173
+ // example on a light/dark switch. The window survives, so its ads must too.
1174
+ win.addEventListener('close', event => {
1175
+ if (event._closeFromActivityForcedToDestroy) return
1176
+ windowClosed = true
1177
+ cancelPendingFullscreenShows()
1178
+ trackingPending = false
1179
+ Ti.App.removeEventListener('pause', onApplicationPause)
1180
+ Ti.App.removeEventListener('resumed', onApplicationResumed)
1181
+ content.removeEventListener('postlayout', layoutPage)
1182
+
1183
+ Admob.removeEventListener('module_initialized', onModuleInitialized)
1184
+ Admob.removeEventListener('consent_complete', onConsentComplete)
1185
+ Admob.removeEventListener(
1186
+ 'privacy_options_dismissed',
1187
+ onPrivacyOptionsDismissed
1188
+ )
1189
+ Admob.removeEventListener('ad_inspector_closed', onAdInspectorClosed)
1190
+ Admob.removeEventListener(
1191
+ 'tracking_authorization_completed',
1192
+ onTrackingAuthorizationCompleted
1193
+ )
1194
+ trackingRequestStarted = false
1195
+
1196
+ destroyBanner()
1197
+ destroyInterstitial()
1198
+ destroyRewarded()
1199
+ destroyRewardedInterstitial()
1200
+ destroyAppOpen()
1201
+ })
1202
+
1203
+ win.addEventListener('open', function onOpen() {
1204
+ win.removeEventListener('open', onOpen)
1205
+ requestExampleConsent(consentMode)
1206
+ })
1207
+ win.open()