@maccesar/titools 5.2.0 → 5.2.2

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