@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.
- package/README.md +44 -12
- package/agents/ti-pro.md +4 -0
- package/commands/ti-audit.md +4 -0
- package/lib/config.js +2 -0
- package/package.json +1 -1
- package/skills/purgetss/SKILL.md +4 -4
- package/skills/purgetss/references/EXAMPLES.md +3 -3
- package/skills/purgetss/references/adopting-purgetss.md +2 -2
- package/skills/purgetss/references/animation-advanced.md +2 -2
- package/skills/purgetss/references/arbitrary-values.md +8 -5
- package/skills/purgetss/references/class-categories.md +5 -5
- package/skills/purgetss/references/class-index-properties.md +6 -6
- package/skills/purgetss/references/class-index.md +2 -2
- package/skills/purgetss/references/cli-commands.md +2 -2
- package/skills/purgetss/references/dynamic-component-creation.md +2 -2
- package/skills/purgetss/references/grid-layout.md +1 -1
- package/skills/purgetss/references/migration-guide.md +34 -4
- package/skills/purgetss/references/multi-density-images.md +6 -6
- package/skills/purgetss/references/platform-modifiers.md +21 -0
- package/skills/purgetss/references/ui-ux-design.md +1 -1
- package/skills/purgetss/references/version-history.md +9 -4
- package/skills/ti-admob-unified/SKILL.md +44 -0
- package/skills/ti-admob-unified/agents/openai.yaml +4 -0
- package/skills/ti-admob-unified/assets/example/app.js +1003 -0
- package/skills/ti-admob-unified/assets/example/lib/ads.js +618 -0
- package/skills/ti-admob-unified/assets/example/library-usage.js +104 -0
- package/skills/ti-admob-unified/references/api-reference.md +592 -0
- package/skills/ti-admob-unified/references/banners.md +435 -0
- package/skills/ti-admob-unified/references/contributing.md +228 -0
- package/skills/ti-admob-unified/references/dependencies.md +85 -0
- package/skills/ti-admob-unified/references/fullscreen.md +223 -0
- package/skills/ti-admob-unified/references/game-monetization.md +17 -0
- package/skills/ti-admob-unified/references/getting-started.md +275 -0
- package/skills/ti-admob-unified/references/index.md +30 -0
- package/skills/ti-admob-unified/references/integration.md +85 -0
- package/skills/ti-admob-unified/references/library.md +189 -0
- package/skills/ti-admob-unified/references/mediation.md +132 -0
- package/skills/ti-admob-unified/references/migration.md +255 -0
- package/skills/ti-admob-unified/references/module-changelog.md +54 -0
- package/skills/ti-admob-unified/references/module-license.txt +21 -0
- package/skills/ti-admob-unified/references/privacy.md +254 -0
- package/skills/ti-admob-unified/references/roadmap.md +146 -0
- package/skills/ti-admob-unified/references/source-notes.md +33 -0
- package/skills/ti-admob-unified/references/source-snapshot.json +440 -0
- package/skills/ti-admob-unified/references/third-party-notices.md +22 -0
- package/skills/ti-game/SKILL.md +10 -1
- package/skills/ti-game/references/api.md +10 -2
- package/skills/ti-game/references/debugging-performance.md +6 -0
- package/skills/ti-game/references/examples.md +11 -1
- package/skills/ti-game/references/patterns.md +24 -2
- package/skills/ti-game/references/physics.md +502 -0
- package/skills/ti-game/references/project-setup.md +23 -1
- package/skills/ti-game/references/recipes-action.md +46 -14
- package/skills/ti-game/references/recipes-adventure-effects.md +12 -1
- package/skills/ti-game/references/roadmap.md +2 -2
- package/skills/ti-game/references/tilemaps.md +2 -1
- package/skills/ti-game-assets/SKILL.md +53 -0
- package/skills/ti-game-assets/agents/openai.yaml +4 -0
- package/skills/ti-game-assets/references/alpha.md +26 -0
- package/skills/ti-game-assets/references/art-direction.md +37 -0
- package/skills/ti-game-assets/references/integration.md +49 -0
- package/skills/ti-game-assets/references/manifest.example.json +429 -0
- package/skills/ti-game-assets/references/manifest.md +39 -0
- package/skills/ti-game-assets/references/offline-audio.md +33 -0
- package/skills/ti-game-assets/references/prompts.md +33 -0
- package/skills/ti-game-assets/references/repeating-textures.md +17 -0
- package/skills/ti-game-assets/references/responsible-ads.md +31 -0
- package/skills/ti-game-assets/references/sound-assets.md +42 -0
- package/skills/ti-game-assets/references/vector-sources.md +21 -0
- package/skills/ti-game-assets/scripts/alpha_tools.py +160 -0
- package/skills/ti-game-assets/scripts/requirements.txt +1 -0
- package/skills/ti-game-assets/scripts/validate_manifest.py +267 -0
- package/skills/ti-synthengine/SKILL.md +37 -13
- package/skills/ti-synthengine/references/api.md +397 -31
- package/skills/ti-synthengine/references/audio-engine.md +235 -14
- package/skills/ti-synthengine/references/examples.md +396 -17
- package/skills/ti-synthengine/references/guide.md +331 -12
- package/skills/ti-synthengine/references/recipes.md +212 -22
- package/skills/ti-synthengine/references/recommendations.md +53 -16
- package/skills/ti-synthengine/references/sound-design.md +359 -15
- package/skills/ti-synthengine/references/source-notes.md +121 -0
- package/skills/ti-synthengine/references/source-snapshot.json +105 -0
- package/skills/ti-synthengine/references/stats-performance.json +2992 -0
- package/skills/ti-synthengine/references/stats-performance.md +38 -0
- 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()
|