@maccesar/titools 5.2.0 → 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 (78) hide show
  1. package/CONTRIBUTING.md +69 -0
  2. package/README.md +80 -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/downloader.js +1 -1
  8. package/package.json +4 -2
  9. package/skills/purgetss/references/multi-density-images.md +1 -1
  10. package/skills/ti-admob-unified/SKILL.md +21 -7
  11. package/skills/ti-admob-unified/assets/example/app.js +229 -25
  12. package/skills/ti-admob-unified/assets/example/lib/ads.js +30 -2
  13. package/skills/ti-admob-unified/assets/example/library-usage.js +33 -1
  14. package/skills/ti-admob-unified/references/api-reference.md +39 -16
  15. package/skills/ti-admob-unified/references/banners.md +9 -9
  16. package/skills/ti-admob-unified/references/contributing.md +14 -9
  17. package/skills/ti-admob-unified/references/dependencies.md +31 -25
  18. package/skills/ti-admob-unified/references/fullscreen.md +43 -28
  19. package/skills/ti-admob-unified/references/game-monetization.md +138 -7
  20. package/skills/ti-admob-unified/references/getting-started.md +75 -31
  21. package/skills/ti-admob-unified/references/index.md +5 -2
  22. package/skills/ti-admob-unified/references/integration.md +8 -6
  23. package/skills/ti-admob-unified/references/library.md +41 -10
  24. package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
  25. package/skills/ti-admob-unified/references/mediation.md +22 -9
  26. package/skills/ti-admob-unified/references/migration.md +11 -7
  27. package/skills/ti-admob-unified/references/module-changelog.md +41 -11
  28. package/skills/ti-admob-unified/references/privacy.md +35 -8
  29. package/skills/ti-admob-unified/references/production.md +67 -0
  30. package/skills/ti-admob-unified/references/rewarded-interstitial.md +101 -0
  31. package/skills/ti-admob-unified/references/roadmap.md +28 -31
  32. package/skills/ti-admob-unified/references/source-notes.md +51 -4
  33. package/skills/ti-admob-unified/references/source-snapshot.json +249 -45
  34. package/skills/ti-admob-unified/references/third-party-notices.md +4 -4
  35. package/skills/ti-game/SKILL.md +23 -6
  36. package/skills/ti-game/references/api.md +34 -30
  37. package/skills/ti-game/references/debugging-performance.md +18 -9
  38. package/skills/ti-game/references/examples.md +103 -20
  39. package/skills/ti-game/references/input.md +101 -0
  40. package/skills/ti-game/references/patterns.md +39 -15
  41. package/skills/ti-game/references/physics.md +80 -52
  42. package/skills/ti-game/references/project-setup.md +118 -56
  43. package/skills/ti-game/references/recipes-action.md +107 -8
  44. package/skills/ti-game/references/recipes-adventure-effects.md +41 -9
  45. package/skills/ti-game/references/roadmap.md +10 -3
  46. package/skills/ti-game/references/source-notes.md +81 -0
  47. package/skills/ti-game/references/tilemaps.md +12 -4
  48. package/skills/ti-synthengine/SKILL.md +81 -95
  49. package/skills/ti-synthengine/assets/example/README.md +84 -0
  50. package/skills/ti-synthengine/assets/example/app.js +1178 -0
  51. package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
  52. package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
  53. package/skills/ti-synthengine/references/advanced-examples.md +174 -0
  54. package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
  55. package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
  56. package/skills/ti-synthengine/references/api.md +434 -380
  57. package/skills/ti-synthengine/references/audio-engine.md +157 -230
  58. package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
  59. package/skills/ti-synthengine/references/examples.md +123 -253
  60. package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
  61. package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
  62. package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
  63. package/skills/ti-synthengine/references/guide.md +188 -156
  64. package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
  65. package/skills/ti-synthengine/references/performance-review.json +1146 -0
  66. package/skills/ti-synthengine/references/physical-validation.json +1457 -0
  67. package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
  68. package/skills/ti-synthengine/references/recipes.md +423 -599
  69. package/skills/ti-synthengine/references/recommendations.md +63 -113
  70. package/skills/ti-synthengine/references/sound-design.md +134 -258
  71. package/skills/ti-synthengine/references/source-notes.md +93 -105
  72. package/skills/ti-synthengine/references/source-snapshot.json +379 -32
  73. package/skills/ti-synthengine/references/stats-performance.md +64 -20
  74. package/skills/ti-synthengine/references/studio-validation.json +6202 -0
  75. package/skills/ti-synthengine/references/troubleshooting.md +109 -121
  76. package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
  77. package/skills/ti-synthengine/references/unified-validation.json +314 -0
  78. package/AGENTS-VERCEL-RESEARCH.md +0 -175
@@ -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
  }
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  const Admob = require('ti.admob.unified')
10
- const ads = require('lib/ads')
10
+ const ads = require('lib/ads') // Classic; Alloy app/lib/ads.js: require('ads')
11
11
 
12
12
  const win = Ti.UI.createWindow({ layout: 'composite', backgroundColor: '#ffffff' })
13
13
  const content = Ti.UI.createScrollView({ top: 0, left: 0, right: 0, bottom: 0 })
@@ -48,6 +48,12 @@ const rewarded = ads.createRewarded({
48
48
  onRewarded: event => grantReward(event.rewardAmount, event.rewardType)
49
49
  })
50
50
 
51
+ const rewardedInterstitial = ads.createRewardedInterstitial({
52
+ window: win,
53
+ adUnitId: Admob.TEST_REWARDED_INTERSTITIAL,
54
+ onRewarded: event => grantReward(event.rewardAmount, event.rewardType)
55
+ })
56
+
51
57
  const appOpen = ads.createAppOpen({
52
58
  window: win,
53
59
  adUnitId: Admob.TEST_APP_OPEN,
@@ -66,6 +72,23 @@ function onRewardAccepted() {
66
72
  rewarded.show()
67
73
  }
68
74
 
75
+ // In production, rewardDescription must describe the reward configured for
76
+ // this placement. Only onRewarded above credits the actual SDK-earned reward.
77
+ function onRewardedTransition(rewardDescription) {
78
+ if (!rewardedInterstitial.isLoaded()) return
79
+ const intro = Ti.UI.createAlertDialog({
80
+ title: 'Rewarded interstitial',
81
+ message: `Complete the ad to earn ${rewardDescription}. You can skip it.`,
82
+ buttonNames: ['Watch ad', 'Skip'], cancel: 1
83
+ })
84
+ function onChoice(event) {
85
+ intro.removeEventListener('click', onChoice)
86
+ if (event.index === 0) rewardedInterstitial.show()
87
+ }
88
+ intro.addEventListener('click', onChoice)
89
+ intro.show()
90
+ }
91
+
69
92
  function onApplicationResumed() {
70
93
  appOpen.show()
71
94
  }
@@ -82,8 +105,13 @@ win.addEventListener('close', event => {
82
105
  })
83
106
  win.open()
84
107
 
108
+ // Start in the active foreground. Enable requestTrackingAuthorization only for
109
+ // a real tracking use case with a localized NSUserTrackingUsageDescription.
110
+ // Keep it false when an AdMob UMP IDFA message already handles ATT.
111
+ Ti.API.info('Current ATT state: ' + JSON.stringify(Admob.getTrackingAuthorizationStatus()))
85
112
  ads.start({
86
113
  consent: { tagForUnderAgeOfConsent: false },
114
+ requestTrackingAuthorization: false,
87
115
  initialization: {}
88
116
  }).then(result => {
89
117
  if (!result.initialized) {
@@ -92,7 +120,10 @@ ads.start({
92
120
 
93
121
  interstitial.load()
94
122
  rewarded.load()
123
+ rewardedInterstitial.load()
95
124
  appOpen.load()
125
+ }).catch(error => {
126
+ Ti.API.warn(`[ads] Startup failed: ${error.message || error}`)
96
127
  })
97
128
 
98
129
  // References kept here to make the intended call sites explicit in this
@@ -100,5 +131,6 @@ ads.start({
100
131
  void createAdaptiveBanner
101
132
  void onNaturalBreak
102
133
  void onRewardAccepted
134
+ void onRewardedTransition
103
135
  void onApplicationResumed
104
136
  void openChildWindow