admanagekit-mcp-server 1.0.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/content/README.md +851 -0
- package/content/docs/AD_FREQUENCY_CONTROL.md +450 -0
- package/content/docs/AD_LOADING_STRATEGIES.md +369 -0
- package/content/docs/API_REFERENCE.md +734 -0
- package/content/docs/APP_PURCHASE_GUIDE.md +704 -0
- package/content/docs/BANNER_AD_IMPROVEMENTS.md +325 -0
- package/content/docs/COMPOSE_INTEGRATION.md +477 -0
- package/content/docs/CONFIGURATION_USAGE.md +168 -0
- package/content/docs/INTERSTITIAL_BUILDER_GUIDE.md +518 -0
- package/content/docs/JAVA_USAGE_GUIDE.md +785 -0
- package/content/docs/LOADING_STRATEGY_EXAMPLES.md +463 -0
- package/content/docs/NATIVE_AD_MANAGER_ENHANCEMENTS.md +347 -0
- package/content/docs/NATIVE_AD_PRELOADING.md +467 -0
- package/content/docs/NATIVE_TEMPLATE_VIEW.md +413 -0
- package/content/docs/app-open-ads.md +412 -0
- package/content/docs/interstitial-ads.md +269 -0
- package/content/docs/native-ads-caching.md +182 -0
- package/content/docs/release-notes/RELEASE_NOTES_v2.1.0.md +156 -0
- package/content/docs/release-notes/RELEASE_NOTES_v2.2.0.md +248 -0
- package/content/docs/release-notes/RELEASE_NOTES_v2.3.0.md +280 -0
- package/content/docs/release-notes/RELEASE_NOTES_v2.5.0.md +73 -0
- package/content/docs/release-notes/RELEASE_NOTES_v2.6.0.md +265 -0
- package/content/docs/release-notes/RELEASE_NOTES_v2.7.0.md +168 -0
- package/content/docs/release-notes/RELEASE_NOTES_v2.8.0.md +153 -0
- package/content/docs/release-notes/RELEASE_NOTES_v2.9.0.md +479 -0
- package/content/docs/release-notes/RELEASE_NOTES_v3.0.0.md +343 -0
- package/content/docs/release-notes/RELEASE_NOTES_v3.1.0.md +131 -0
- package/content/docs/release-notes/RELEASE_NOTES_v3.3.0.md +252 -0
- package/content/docs/release-notes/RELEASE_NOTES_v3.3.2.md +215 -0
- package/content/docs/release-notes/RELEASE_NOTES_v3.3.3.md +116 -0
- package/content/docs/release-notes/RELEASE_NOTES_v3.3.4.md +142 -0
- package/content/docs/release-notes/RELEASE_NOTES_v3.3.5.md +94 -0
- package/content/docs/rewarded-ads.md +623 -0
- package/content/wiki/Ad-Loading-Strategies.md +209 -0
- package/content/wiki/App-Open-Ads.md +189 -0
- package/content/wiki/Banner-Ads.md +133 -0
- package/content/wiki/Billing-Integration.md +69 -0
- package/content/wiki/Configuration.md +252 -0
- package/content/wiki/Consumable-Products.md +166 -0
- package/content/wiki/Home.md +144 -0
- package/content/wiki/Interstitial-Ads.md +313 -0
- package/content/wiki/Jetpack-Compose.md +270 -0
- package/content/wiki/NativeAdManager.md +220 -0
- package/content/wiki/Purchase-Categories.md +94 -0
- package/content/wiki/Rewarded-Ads.md +420 -0
- package/content/wiki/Subscription-Upgrades.md +278 -0
- package/content/wiki/Subscriptions.md +368 -0
- package/content/wiki/_Footer.md +3 -0
- package/content/wiki/_Sidebar.md +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +18 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/code-generation.d.ts +2 -0
- package/dist/tools/code-generation.js +264 -0
- package/dist/tools/code-generation.js.map +1 -0
- package/dist/tools/documentation.d.ts +2 -0
- package/dist/tools/documentation.js +202 -0
- package/dist/tools/documentation.js.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.js +83 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/doc-loader.d.ts +17 -0
- package/dist/utils/doc-loader.js +245 -0
- package/dist/utils/doc-loader.js.map +1 -0
- package/dist/utils/search.d.ts +2 -0
- package/dist/utils/search.js +62 -0
- package/dist/utils/search.js.map +1 -0
- package/dist/utils/templates.d.ts +60 -0
- package/dist/utils/templates.js +793 -0
- package/dist/utils/templates.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# Rewarded Ads
|
|
2
|
+
|
|
3
|
+
RewardedAdManager provides a comprehensive solution for integrating rewarded video ads with automatic retry, analytics tracking, and premium user handling.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Automatic Retry**: Exponential backoff on load failures
|
|
8
|
+
- **Premium User Handling**: Automatically skips ads for purchased users
|
|
9
|
+
- **Timeout Support**: For splash screens and time-sensitive scenarios
|
|
10
|
+
- **Firebase Analytics**: Tracks requests, fills, impressions, and show rates
|
|
11
|
+
- **Configurable Auto-Reload**: Automatically reload after ad dismissal
|
|
12
|
+
- **Full Lifecycle Callbacks**: Reward earned, shown, dismissed, clicked, failed
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### 1. Initialize
|
|
17
|
+
|
|
18
|
+
```kotlin
|
|
19
|
+
// In Application.onCreate() or Activity
|
|
20
|
+
RewardedAdManager.initialize(context, "ca-app-pub-xxx/yyy")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Show Rewarded Ad
|
|
24
|
+
|
|
25
|
+
```kotlin
|
|
26
|
+
if (RewardedAdManager.isAdLoaded()) {
|
|
27
|
+
RewardedAdManager.showAd(activity, object : RewardedAdManager.RewardedAdCallback {
|
|
28
|
+
override fun onRewardEarned(rewardType: String, rewardAmount: Int) {
|
|
29
|
+
// Grant reward to user
|
|
30
|
+
userCoins += rewardAmount
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override fun onAdDismissed() {
|
|
34
|
+
// Continue app flow
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
} else {
|
|
38
|
+
// Ad not ready - show alternative or wait
|
|
39
|
+
Toast.makeText(context, "Ad not ready yet", Toast.LENGTH_SHORT).show()
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Callback Interfaces
|
|
44
|
+
|
|
45
|
+
### RewardedAdCallback (Recommended)
|
|
46
|
+
|
|
47
|
+
Full lifecycle callback with all events:
|
|
48
|
+
|
|
49
|
+
```kotlin
|
|
50
|
+
interface RewardedAdCallback {
|
|
51
|
+
// Required
|
|
52
|
+
fun onRewardEarned(rewardType: String, rewardAmount: Int)
|
|
53
|
+
fun onAdDismissed()
|
|
54
|
+
|
|
55
|
+
// Optional (default empty implementations)
|
|
56
|
+
fun onAdShowed() {}
|
|
57
|
+
fun onAdFailedToShow(error: AdError) {}
|
|
58
|
+
fun onAdClicked() {}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### OnRewardedAdLoadCallback
|
|
63
|
+
|
|
64
|
+
For load-specific events:
|
|
65
|
+
|
|
66
|
+
```kotlin
|
|
67
|
+
interface OnRewardedAdLoadCallback {
|
|
68
|
+
fun onAdLoaded()
|
|
69
|
+
fun onAdFailedToLoad(error: LoadAdError)
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Loading Methods
|
|
74
|
+
|
|
75
|
+
### Basic Load
|
|
76
|
+
|
|
77
|
+
```kotlin
|
|
78
|
+
// Simple load (auto-retries on failure)
|
|
79
|
+
RewardedAdManager.loadRewardedAd(context)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Load with Callback
|
|
83
|
+
|
|
84
|
+
```kotlin
|
|
85
|
+
RewardedAdManager.loadRewardedAd(context, object : RewardedAdManager.OnRewardedAdLoadCallback {
|
|
86
|
+
override fun onAdLoaded() {
|
|
87
|
+
// Ad ready to show
|
|
88
|
+
showWatchAdButton()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override fun onAdFailedToLoad(error: LoadAdError) {
|
|
92
|
+
// Handle error
|
|
93
|
+
Log.e("Ads", "Failed: ${error.message}")
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Load with Timeout (Splash Screens)
|
|
99
|
+
|
|
100
|
+
Perfect for splash screens where you need to proceed after a timeout:
|
|
101
|
+
|
|
102
|
+
```kotlin
|
|
103
|
+
RewardedAdManager.loadRewardedAdWithTimeout(
|
|
104
|
+
context = this,
|
|
105
|
+
timeoutMillis = 5000, // 5 seconds
|
|
106
|
+
callback = object : RewardedAdManager.OnRewardedAdLoadCallback {
|
|
107
|
+
override fun onAdLoaded() {
|
|
108
|
+
// Show ad or proceed
|
|
109
|
+
showRewardedOffer()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override fun onAdFailedToLoad(error: LoadAdError) {
|
|
113
|
+
// Timeout or error - proceed anyway
|
|
114
|
+
navigateToMain()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Note**: If the ad loads after timeout, it's saved for next use (not wasted).
|
|
121
|
+
|
|
122
|
+
## Display Methods
|
|
123
|
+
|
|
124
|
+
### Show with Callback
|
|
125
|
+
|
|
126
|
+
```kotlin
|
|
127
|
+
RewardedAdManager.showAd(
|
|
128
|
+
activity = this,
|
|
129
|
+
callback = object : RewardedAdManager.RewardedAdCallback {
|
|
130
|
+
override fun onRewardEarned(rewardType: String, rewardAmount: Int) {
|
|
131
|
+
// Grant reward
|
|
132
|
+
addCoins(rewardAmount)
|
|
133
|
+
showRewardAnimation()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
override fun onAdDismissed() {
|
|
137
|
+
// User closed the ad
|
|
138
|
+
continueGame()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override fun onAdShowed() {
|
|
142
|
+
// Ad is now visible
|
|
143
|
+
pauseGame()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
override fun onAdFailedToShow(error: AdError) {
|
|
147
|
+
// Couldn't show - maybe show alternative
|
|
148
|
+
Log.e("Ads", "Show failed: ${error.message}")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
override fun onAdClicked() {
|
|
152
|
+
// User clicked the ad
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
autoReload = true // Reload after dismissal
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Control Auto-Reload
|
|
160
|
+
|
|
161
|
+
```kotlin
|
|
162
|
+
// Disable auto-reload for this show
|
|
163
|
+
RewardedAdManager.showAd(activity, callback, autoReload = false)
|
|
164
|
+
|
|
165
|
+
// Or configure globally
|
|
166
|
+
AdManageKitConfig.rewardedAutoReload = false
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## State Checking
|
|
170
|
+
|
|
171
|
+
```kotlin
|
|
172
|
+
// Check if ad is ready
|
|
173
|
+
if (RewardedAdManager.isAdLoaded()) {
|
|
174
|
+
showWatchAdButton()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check if loading
|
|
178
|
+
if (RewardedAdManager.isLoading()) {
|
|
179
|
+
showLoadingIndicator()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if currently showing
|
|
183
|
+
if (RewardedAdManager.isShowingAd()) {
|
|
184
|
+
// Don't interrupt
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Preloading
|
|
189
|
+
|
|
190
|
+
Preload during natural pauses to improve show rate:
|
|
191
|
+
|
|
192
|
+
```kotlin
|
|
193
|
+
// In Activity.onResume()
|
|
194
|
+
override fun onResume() {
|
|
195
|
+
super.onResume()
|
|
196
|
+
RewardedAdManager.preload(this)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// After completing a level
|
|
200
|
+
fun onLevelComplete() {
|
|
201
|
+
showResults()
|
|
202
|
+
RewardedAdManager.preload(this) // Preload for next opportunity
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Analytics & Debugging
|
|
207
|
+
|
|
208
|
+
### Get Statistics
|
|
209
|
+
|
|
210
|
+
```kotlin
|
|
211
|
+
val stats = RewardedAdManager.getAdStats()
|
|
212
|
+
|
|
213
|
+
// Available stats:
|
|
214
|
+
// - session_requests: Int
|
|
215
|
+
// - session_fills: Int
|
|
216
|
+
// - session_impressions: Int
|
|
217
|
+
// - fill_rate_percent: Float
|
|
218
|
+
// - show_rate_percent: Float
|
|
219
|
+
// - is_loaded: Boolean
|
|
220
|
+
// - is_loading: Boolean
|
|
221
|
+
// - retry_attempts: Int
|
|
222
|
+
|
|
223
|
+
Log.d("Ads", """
|
|
224
|
+
Requests: ${stats["session_requests"]}
|
|
225
|
+
Fills: ${stats["session_fills"]}
|
|
226
|
+
Impressions: ${stats["session_impressions"]}
|
|
227
|
+
Fill Rate: ${stats["fill_rate_percent"]}%
|
|
228
|
+
Show Rate: ${stats["show_rate_percent"]}%
|
|
229
|
+
""".trimIndent())
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Reset Statistics
|
|
233
|
+
|
|
234
|
+
```kotlin
|
|
235
|
+
// Reset at start of new session
|
|
236
|
+
RewardedAdManager.resetAdStats()
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Firebase Events
|
|
240
|
+
|
|
241
|
+
RewardedAdManager automatically logs these Firebase Analytics events:
|
|
242
|
+
|
|
243
|
+
| Event | Description |
|
|
244
|
+
|-------|-------------|
|
|
245
|
+
| `ad_request` | Ad load requested |
|
|
246
|
+
| `ad_fill` | Ad loaded successfully |
|
|
247
|
+
| `ad_failed_to_load` | Ad failed to load |
|
|
248
|
+
| `ad_impression` | Standard impression event |
|
|
249
|
+
| `ad_impression_detailed` | Detailed with fill/show rates |
|
|
250
|
+
| `ad_dismissed` | User closed the ad |
|
|
251
|
+
| `ad_failed_to_show` | Ad couldn't be shown |
|
|
252
|
+
| `ad_paid_event` | Revenue event for tROAS |
|
|
253
|
+
| `rewarded_ad_reward` | User earned reward |
|
|
254
|
+
|
|
255
|
+
## Automatic Retry
|
|
256
|
+
|
|
257
|
+
RewardedAdManager automatically retries failed loads using exponential backoff:
|
|
258
|
+
|
|
259
|
+
```kotlin
|
|
260
|
+
// Configure retry behavior
|
|
261
|
+
AdManageKitConfig.apply {
|
|
262
|
+
autoRetryFailedAds = true // Enable auto-retry (default: true)
|
|
263
|
+
maxRetryAttempts = 3 // Max attempts (default: 3)
|
|
264
|
+
baseRetryDelay = 1.seconds // Base delay (default: 1s)
|
|
265
|
+
maxRetryDelay = 30.seconds // Max delay (default: 30s)
|
|
266
|
+
enableExponentialBackoff = true // Exponential backoff (default: true)
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Retry Schedule** (with exponential backoff):
|
|
271
|
+
- Attempt 1: 1 second delay
|
|
272
|
+
- Attempt 2: 2 seconds delay
|
|
273
|
+
- Attempt 3: 4 seconds delay
|
|
274
|
+
- (capped at maxRetryDelay)
|
|
275
|
+
|
|
276
|
+
**Manual Load Cancels Retry**: If you manually call `loadRewardedAd()` while a retry is pending, the pending retry is cancelled and a fresh load starts.
|
|
277
|
+
|
|
278
|
+
## Premium User Handling
|
|
279
|
+
|
|
280
|
+
RewardedAdManager automatically checks purchase status:
|
|
281
|
+
|
|
282
|
+
```kotlin
|
|
283
|
+
// If user has purchased premium, these return early:
|
|
284
|
+
RewardedAdManager.loadRewardedAd(context) // Skips loading
|
|
285
|
+
RewardedAdManager.isAdLoaded() // Returns false
|
|
286
|
+
RewardedAdManager.showAd(...) // Calls onAdDismissed() immediately
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
This requires setting up BillingConfig:
|
|
290
|
+
|
|
291
|
+
```kotlin
|
|
292
|
+
// In Application.onCreate()
|
|
293
|
+
BillingConfig.setPurchaseProvider(BillingPurchaseProvider())
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Best Practices
|
|
297
|
+
|
|
298
|
+
### 1. Initialize Early
|
|
299
|
+
|
|
300
|
+
```kotlin
|
|
301
|
+
class MyApplication : Application() {
|
|
302
|
+
override fun onCreate() {
|
|
303
|
+
super.onCreate()
|
|
304
|
+
RewardedAdManager.initialize(this, "ca-app-pub-xxx/yyy")
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### 2. Preload Strategically
|
|
310
|
+
|
|
311
|
+
```kotlin
|
|
312
|
+
// Good places to preload:
|
|
313
|
+
// - After showing an ad (if autoReload is off)
|
|
314
|
+
// - When entering a screen with "Watch Ad" button
|
|
315
|
+
// - After user completes an action
|
|
316
|
+
// - In onResume()
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### 3. Handle All States
|
|
320
|
+
|
|
321
|
+
```kotlin
|
|
322
|
+
fun onWatchAdClicked() {
|
|
323
|
+
when {
|
|
324
|
+
RewardedAdManager.isAdLoaded() -> {
|
|
325
|
+
RewardedAdManager.showAd(activity, callback)
|
|
326
|
+
}
|
|
327
|
+
RewardedAdManager.isLoading() -> {
|
|
328
|
+
showToast("Ad loading, please wait...")
|
|
329
|
+
}
|
|
330
|
+
else -> {
|
|
331
|
+
showToast("No ad available")
|
|
332
|
+
RewardedAdManager.loadRewardedAd(context)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### 4. Don't Block UI
|
|
339
|
+
|
|
340
|
+
```kotlin
|
|
341
|
+
// DON'T: Block user while loading
|
|
342
|
+
fun showAd() {
|
|
343
|
+
showBlockingDialog()
|
|
344
|
+
RewardedAdManager.loadRewardedAd(context, callback) // Bad UX
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// DO: Use preloading and show when ready
|
|
348
|
+
fun showAd() {
|
|
349
|
+
if (RewardedAdManager.isAdLoaded()) {
|
|
350
|
+
RewardedAdManager.showAd(activity, callback)
|
|
351
|
+
} else {
|
|
352
|
+
showToast("Ad not ready")
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Migration from Legacy API
|
|
358
|
+
|
|
359
|
+
### Before (Legacy)
|
|
360
|
+
|
|
361
|
+
```kotlin
|
|
362
|
+
RewardedAdManager.showAd(
|
|
363
|
+
activity,
|
|
364
|
+
OnUserEarnedRewardListener { reward ->
|
|
365
|
+
addCoins(reward.amount)
|
|
366
|
+
},
|
|
367
|
+
object : RewardedAdManager.OnAdDismissedListener {
|
|
368
|
+
override fun onAdDismissed() {
|
|
369
|
+
continueFlow()
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### After (New API)
|
|
376
|
+
|
|
377
|
+
```kotlin
|
|
378
|
+
RewardedAdManager.showAd(activity, object : RewardedAdManager.RewardedAdCallback {
|
|
379
|
+
override fun onRewardEarned(rewardType: String, rewardAmount: Int) {
|
|
380
|
+
addCoins(rewardAmount)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
override fun onAdDismissed() {
|
|
384
|
+
continueFlow()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Now you also get:
|
|
388
|
+
override fun onAdShowed() { pauseGame() }
|
|
389
|
+
override fun onAdFailedToShow(error: AdError) { handleError(error) }
|
|
390
|
+
override fun onAdClicked() { trackClick() }
|
|
391
|
+
})
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Troubleshooting
|
|
395
|
+
|
|
396
|
+
### Ad Not Loading
|
|
397
|
+
|
|
398
|
+
1. Check internet connection
|
|
399
|
+
2. Verify ad unit ID is correct
|
|
400
|
+
3. Check if user is premium: `BillingConfig.getPurchaseProvider().isPurchased()`
|
|
401
|
+
4. Check retry status: `RewardedAdManager.getAdStats()["retry_attempts"]`
|
|
402
|
+
|
|
403
|
+
### Ad Not Showing
|
|
404
|
+
|
|
405
|
+
1. Check if loaded: `RewardedAdManager.isAdLoaded()`
|
|
406
|
+
2. Check if already showing: `RewardedAdManager.isShowingAd()`
|
|
407
|
+
3. Verify Activity is not finishing
|
|
408
|
+
|
|
409
|
+
### Low Fill Rate
|
|
410
|
+
|
|
411
|
+
1. Check `getAdStats()["fill_rate_percent"]`
|
|
412
|
+
2. Consider using multiple ad networks via mediation
|
|
413
|
+
3. Check if test mode is properly configured
|
|
414
|
+
|
|
415
|
+
## See Also
|
|
416
|
+
|
|
417
|
+
- [API Reference](../docs/API_REFERENCE.md#rewardedadmanager)
|
|
418
|
+
- [Interstitial Ads](../docs/interstitial-ads.md)
|
|
419
|
+
- [Ad Loading Strategies](../docs/AD_LOADING_STRATEGIES.md)
|
|
420
|
+
- [Billing Integration](Billing-Integration.md)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Subscription Upgrades & Downgrades
|
|
2
|
+
|
|
3
|
+
AdManageKit v2.9.0 provides full support for subscription tier changes with configurable proration modes.
|
|
4
|
+
|
|
5
|
+
## Quick Methods
|
|
6
|
+
|
|
7
|
+
### Upgrade
|
|
8
|
+
|
|
9
|
+
Upgrades use `CHARGE_PRORATED_PRICE` mode - user pays the price difference immediately.
|
|
10
|
+
|
|
11
|
+
```kotlin
|
|
12
|
+
// User is on "basic_monthly", upgrade to "premium_monthly"
|
|
13
|
+
AppPurchase.getInstance().upgradeSubscription(activity, "premium_monthly")
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Downgrade
|
|
17
|
+
|
|
18
|
+
Downgrades use `DEFERRED` mode - change takes effect at the next renewal date.
|
|
19
|
+
|
|
20
|
+
```kotlin
|
|
21
|
+
// User is on "premium_monthly", downgrade to "basic_monthly"
|
|
22
|
+
AppPurchase.getInstance().downgradeSubscription(activity, "basic_monthly")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Full Control Method
|
|
26
|
+
|
|
27
|
+
For precise control over which subscription to replace and how:
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
AppPurchase.getInstance().changeSubscription(
|
|
31
|
+
activity,
|
|
32
|
+
"premium_monthly", // Current subscription ID
|
|
33
|
+
"premium_yearly", // New subscription ID
|
|
34
|
+
SubscriptionReplacementMode.CHARGE_PRORATED_PRICE // Proration mode
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Replacement Modes
|
|
39
|
+
|
|
40
|
+
| Mode | Effect | Best For |
|
|
41
|
+
|------|--------|----------|
|
|
42
|
+
| `CHARGE_PRORATED_PRICE` | User pays price difference immediately | **Upgrades** |
|
|
43
|
+
| `DEFERRED` | Change at next renewal, no immediate charge | **Downgrades** |
|
|
44
|
+
| `WITH_TIME_PRORATION` | Immediate change, remaining value credited | Either |
|
|
45
|
+
| `CHARGE_FULL_PRICE` | Immediate change, full price charged | Revenue maximization |
|
|
46
|
+
| `WITHOUT_PRORATION` | Immediate change, billing date unchanged | Same-tier changes |
|
|
47
|
+
|
|
48
|
+
### Mode Details
|
|
49
|
+
|
|
50
|
+
#### CHARGE_PRORATED_PRICE (Recommended for Upgrades)
|
|
51
|
+
|
|
52
|
+
```kotlin
|
|
53
|
+
// User pays difference between old and new subscription
|
|
54
|
+
// Example: Monthly $5 → Yearly $50
|
|
55
|
+
// User pays: $50 - ($5 * remaining_days/30)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### DEFERRED (Recommended for Downgrades)
|
|
59
|
+
|
|
60
|
+
```kotlin
|
|
61
|
+
// No immediate change - user keeps current subscription until renewal
|
|
62
|
+
// At renewal, switches to new (lower) subscription
|
|
63
|
+
// Good for user experience - they get what they paid for
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### WITH_TIME_PRORATION
|
|
67
|
+
|
|
68
|
+
```kotlin
|
|
69
|
+
// Immediate switch with time-based credit
|
|
70
|
+
// Remaining value of old subscription credited toward new
|
|
71
|
+
// Useful for same-price tier changes
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### CHARGE_FULL_PRICE
|
|
75
|
+
|
|
76
|
+
```kotlin
|
|
77
|
+
// Immediate switch, full price charged
|
|
78
|
+
// Old subscription remaining value lost
|
|
79
|
+
// Maximum immediate revenue
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### WITHOUT_PRORATION
|
|
83
|
+
|
|
84
|
+
```kotlin
|
|
85
|
+
// Immediate switch, no price adjustment
|
|
86
|
+
// Billing cycle remains the same
|
|
87
|
+
// Use for same-price tier switches
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Implementation Example
|
|
91
|
+
|
|
92
|
+
### Subscription Tier Screen
|
|
93
|
+
|
|
94
|
+
```kotlin
|
|
95
|
+
class SubscriptionTiersActivity : AppCompatActivity() {
|
|
96
|
+
|
|
97
|
+
private val tiers = listOf(
|
|
98
|
+
Tier("basic_monthly", "Basic", "$4.99/mo"),
|
|
99
|
+
Tier("premium_monthly", "Premium", "$9.99/mo"),
|
|
100
|
+
Tier("premium_yearly", "Premium Annual", "$99.99/yr")
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
104
|
+
super.onCreate(savedInstanceState)
|
|
105
|
+
setContentView(R.layout.activity_tiers)
|
|
106
|
+
|
|
107
|
+
val currentSub = AppPurchase.getInstance().getActiveSubscriptions().firstOrNull()
|
|
108
|
+
val currentTierId = currentSub?.getFirstProductId()
|
|
109
|
+
|
|
110
|
+
tiers.forEach { tier ->
|
|
111
|
+
val button = createTierButton(tier)
|
|
112
|
+
|
|
113
|
+
when {
|
|
114
|
+
tier.id == currentTierId -> {
|
|
115
|
+
button.text = "Current Plan"
|
|
116
|
+
button.isEnabled = false
|
|
117
|
+
}
|
|
118
|
+
isUpgrade(currentTierId, tier.id) -> {
|
|
119
|
+
button.text = "Upgrade to ${tier.name}"
|
|
120
|
+
button.setOnClickListener { upgradeTo(tier.id) }
|
|
121
|
+
}
|
|
122
|
+
isDowngrade(currentTierId, tier.id) -> {
|
|
123
|
+
button.text = "Downgrade to ${tier.name}"
|
|
124
|
+
button.setOnClickListener { downgradeTo(tier.id) }
|
|
125
|
+
}
|
|
126
|
+
else -> {
|
|
127
|
+
button.text = "Subscribe to ${tier.name}"
|
|
128
|
+
button.setOnClickListener { subscribeTo(tier.id) }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private fun upgradeTo(tierId: String) {
|
|
135
|
+
AppPurchase.getInstance().upgradeSubscription(this, tierId)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private fun downgradeTo(tierId: String) {
|
|
139
|
+
AppPurchase.getInstance().downgradeSubscription(this, tierId)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private fun subscribeTo(tierId: String) {
|
|
143
|
+
AppPurchase.getInstance().subscribe(this, tierId)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private fun isUpgrade(current: String?, new: String): Boolean {
|
|
147
|
+
val currentIndex = tiers.indexOfFirst { it.id == current }
|
|
148
|
+
val newIndex = tiers.indexOfFirst { it.id == new }
|
|
149
|
+
return currentIndex != -1 && newIndex > currentIndex
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private fun isDowngrade(current: String?, new: String): Boolean {
|
|
153
|
+
val currentIndex = tiers.indexOfFirst { it.id == current }
|
|
154
|
+
val newIndex = tiers.indexOfFirst { it.id == new }
|
|
155
|
+
return currentIndex != -1 && newIndex < currentIndex
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Handle Result
|
|
161
|
+
|
|
162
|
+
The upgrade/downgrade uses the same purchase flow:
|
|
163
|
+
|
|
164
|
+
```kotlin
|
|
165
|
+
AppPurchase.getInstance().setPurchaseListener(object : PurchaseListener {
|
|
166
|
+
override fun onProductPurchased(orderId: String?, originalJson: String?) {
|
|
167
|
+
// Subscription changed successfully
|
|
168
|
+
Toast.makeText(this, "Subscription updated!", Toast.LENGTH_SHORT).show()
|
|
169
|
+
updateUI()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
override fun displayErrorMessage(errorMessage: String?) {
|
|
173
|
+
Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
override fun onUserCancelBilling() {
|
|
177
|
+
// User cancelled - no action needed
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Low-Level API
|
|
183
|
+
|
|
184
|
+
For direct access to the purchase token:
|
|
185
|
+
|
|
186
|
+
```kotlin
|
|
187
|
+
val subscription = AppPurchase.getInstance().getSubscription("premium_monthly")
|
|
188
|
+
if (subscription != null) {
|
|
189
|
+
AppPurchase.getInstance().updateSubscription(
|
|
190
|
+
activity,
|
|
191
|
+
"premium_yearly", // New subscription ID
|
|
192
|
+
subscription.purchaseToken, // Current purchase token
|
|
193
|
+
SubscriptionReplacementMode.WITH_TIME_PRORATION
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Common Scenarios
|
|
199
|
+
|
|
200
|
+
### Monthly → Yearly (Same Tier)
|
|
201
|
+
|
|
202
|
+
```kotlin
|
|
203
|
+
// User wants annual billing for same features
|
|
204
|
+
AppPurchase.getInstance().changeSubscription(
|
|
205
|
+
activity,
|
|
206
|
+
"premium_monthly",
|
|
207
|
+
"premium_yearly",
|
|
208
|
+
SubscriptionReplacementMode.CHARGE_PRORATED_PRICE // Pay difference for remaining month
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Yearly → Monthly (Same Tier)
|
|
213
|
+
|
|
214
|
+
```kotlin
|
|
215
|
+
// User wants monthly billing for same features
|
|
216
|
+
AppPurchase.getInstance().changeSubscription(
|
|
217
|
+
activity,
|
|
218
|
+
"premium_yearly",
|
|
219
|
+
"premium_monthly",
|
|
220
|
+
SubscriptionReplacementMode.DEFERRED // Change at renewal
|
|
221
|
+
)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Basic → Premium (Upgrade)
|
|
225
|
+
|
|
226
|
+
```kotlin
|
|
227
|
+
AppPurchase.getInstance().changeSubscription(
|
|
228
|
+
activity,
|
|
229
|
+
"basic_monthly",
|
|
230
|
+
"premium_monthly",
|
|
231
|
+
SubscriptionReplacementMode.CHARGE_PRORATED_PRICE
|
|
232
|
+
)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Premium → Basic (Downgrade)
|
|
236
|
+
|
|
237
|
+
```kotlin
|
|
238
|
+
AppPurchase.getInstance().changeSubscription(
|
|
239
|
+
activity,
|
|
240
|
+
"premium_monthly",
|
|
241
|
+
"basic_monthly",
|
|
242
|
+
SubscriptionReplacementMode.DEFERRED
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Error Handling
|
|
247
|
+
|
|
248
|
+
```kotlin
|
|
249
|
+
val result = AppPurchase.getInstance().changeSubscription(
|
|
250
|
+
activity,
|
|
251
|
+
"current_sub",
|
|
252
|
+
"new_sub",
|
|
253
|
+
SubscriptionReplacementMode.CHARGE_PRORATED_PRICE
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
when {
|
|
257
|
+
result == "OK" -> {
|
|
258
|
+
// Billing flow started successfully
|
|
259
|
+
}
|
|
260
|
+
result.contains("not found") -> {
|
|
261
|
+
// Subscription ID invalid or not loaded
|
|
262
|
+
Log.e("Billing", "Invalid subscription ID")
|
|
263
|
+
}
|
|
264
|
+
result.contains("No active subscription") -> {
|
|
265
|
+
// User doesn't have the source subscription
|
|
266
|
+
// Fall back to regular subscribe
|
|
267
|
+
AppPurchase.getInstance().subscribe(activity, "new_sub")
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Best Practices
|
|
273
|
+
|
|
274
|
+
1. **Use DEFERRED for downgrades** - Users appreciate getting what they paid for
|
|
275
|
+
2. **Use CHARGE_PRORATED_PRICE for upgrades** - Immediate access, fair pricing
|
|
276
|
+
3. **Show clear pricing** - Display what user will pay before confirming
|
|
277
|
+
4. **Handle multiple subscriptions** - User might have multiple active subscriptions
|
|
278
|
+
5. **Test all scenarios** - Use Google Play Console's test tracks
|