@maccesar/titools 5.1.0 → 5.2.0

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