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.
Files changed (74) hide show
  1. package/content/README.md +851 -0
  2. package/content/docs/AD_FREQUENCY_CONTROL.md +450 -0
  3. package/content/docs/AD_LOADING_STRATEGIES.md +369 -0
  4. package/content/docs/API_REFERENCE.md +734 -0
  5. package/content/docs/APP_PURCHASE_GUIDE.md +704 -0
  6. package/content/docs/BANNER_AD_IMPROVEMENTS.md +325 -0
  7. package/content/docs/COMPOSE_INTEGRATION.md +477 -0
  8. package/content/docs/CONFIGURATION_USAGE.md +168 -0
  9. package/content/docs/INTERSTITIAL_BUILDER_GUIDE.md +518 -0
  10. package/content/docs/JAVA_USAGE_GUIDE.md +785 -0
  11. package/content/docs/LOADING_STRATEGY_EXAMPLES.md +463 -0
  12. package/content/docs/NATIVE_AD_MANAGER_ENHANCEMENTS.md +347 -0
  13. package/content/docs/NATIVE_AD_PRELOADING.md +467 -0
  14. package/content/docs/NATIVE_TEMPLATE_VIEW.md +413 -0
  15. package/content/docs/app-open-ads.md +412 -0
  16. package/content/docs/interstitial-ads.md +269 -0
  17. package/content/docs/native-ads-caching.md +182 -0
  18. package/content/docs/release-notes/RELEASE_NOTES_v2.1.0.md +156 -0
  19. package/content/docs/release-notes/RELEASE_NOTES_v2.2.0.md +248 -0
  20. package/content/docs/release-notes/RELEASE_NOTES_v2.3.0.md +280 -0
  21. package/content/docs/release-notes/RELEASE_NOTES_v2.5.0.md +73 -0
  22. package/content/docs/release-notes/RELEASE_NOTES_v2.6.0.md +265 -0
  23. package/content/docs/release-notes/RELEASE_NOTES_v2.7.0.md +168 -0
  24. package/content/docs/release-notes/RELEASE_NOTES_v2.8.0.md +153 -0
  25. package/content/docs/release-notes/RELEASE_NOTES_v2.9.0.md +479 -0
  26. package/content/docs/release-notes/RELEASE_NOTES_v3.0.0.md +343 -0
  27. package/content/docs/release-notes/RELEASE_NOTES_v3.1.0.md +131 -0
  28. package/content/docs/release-notes/RELEASE_NOTES_v3.3.0.md +252 -0
  29. package/content/docs/release-notes/RELEASE_NOTES_v3.3.2.md +215 -0
  30. package/content/docs/release-notes/RELEASE_NOTES_v3.3.3.md +116 -0
  31. package/content/docs/release-notes/RELEASE_NOTES_v3.3.4.md +142 -0
  32. package/content/docs/release-notes/RELEASE_NOTES_v3.3.5.md +94 -0
  33. package/content/docs/rewarded-ads.md +623 -0
  34. package/content/wiki/Ad-Loading-Strategies.md +209 -0
  35. package/content/wiki/App-Open-Ads.md +189 -0
  36. package/content/wiki/Banner-Ads.md +133 -0
  37. package/content/wiki/Billing-Integration.md +69 -0
  38. package/content/wiki/Configuration.md +252 -0
  39. package/content/wiki/Consumable-Products.md +166 -0
  40. package/content/wiki/Home.md +144 -0
  41. package/content/wiki/Interstitial-Ads.md +313 -0
  42. package/content/wiki/Jetpack-Compose.md +270 -0
  43. package/content/wiki/NativeAdManager.md +220 -0
  44. package/content/wiki/Purchase-Categories.md +94 -0
  45. package/content/wiki/Rewarded-Ads.md +420 -0
  46. package/content/wiki/Subscription-Upgrades.md +278 -0
  47. package/content/wiki/Subscriptions.md +368 -0
  48. package/content/wiki/_Footer.md +3 -0
  49. package/content/wiki/_Sidebar.md +18 -0
  50. package/dist/index.d.ts +2 -0
  51. package/dist/index.js +6 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/server.d.ts +1 -0
  54. package/dist/server.js +18 -0
  55. package/dist/server.js.map +1 -0
  56. package/dist/tools/code-generation.d.ts +2 -0
  57. package/dist/tools/code-generation.js +264 -0
  58. package/dist/tools/code-generation.js.map +1 -0
  59. package/dist/tools/documentation.d.ts +2 -0
  60. package/dist/tools/documentation.js +202 -0
  61. package/dist/tools/documentation.js.map +1 -0
  62. package/dist/types.d.ts +18 -0
  63. package/dist/types.js +83 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/utils/doc-loader.d.ts +17 -0
  66. package/dist/utils/doc-loader.js +245 -0
  67. package/dist/utils/doc-loader.js.map +1 -0
  68. package/dist/utils/search.d.ts +2 -0
  69. package/dist/utils/search.js +62 -0
  70. package/dist/utils/search.js.map +1 -0
  71. package/dist/utils/templates.d.ts +60 -0
  72. package/dist/utils/templates.js +793 -0
  73. package/dist/utils/templates.js.map +1 -0
  74. 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