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,704 @@
1
+ # AppPurchase - Complete Billing Integration Guide
2
+
3
+ `AppPurchase` is a comprehensive wrapper for Google Play Billing Library v8, providing simplified APIs for in-app purchases, subscriptions, and purchase management.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Setup](#setup)
8
+ - [Product Configuration](#product-configuration)
9
+ - [Making Purchases](#making-purchases)
10
+ - [Consumable Products](#consumable-products)
11
+ - [Subscriptions](#subscriptions)
12
+ - [Subscription Upgrade/Downgrade](#subscription-upgradedowngrade)
13
+ - [Purchase State Checking](#purchase-state-checking)
14
+ - [Purchase History Tracking](#purchase-history-tracking)
15
+ - [Server-Side Verification](#server-side-verification)
16
+ - [Best Practices](#best-practices)
17
+
18
+ ---
19
+
20
+ ## Setup
21
+
22
+ ### 1. Add Dependencies
23
+
24
+ ```groovy
25
+ implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-billing:v2.9.0'
26
+ implementation 'com.github.i2hammad.AdManageKit:ad-manage-kit-core:v2.9.0'
27
+ ```
28
+
29
+ ### 2. Initialize in Application Class
30
+
31
+ ```kotlin
32
+ class MyApp : Application() {
33
+ override fun onCreate() {
34
+ super.onCreate()
35
+
36
+ // Define your products
37
+ val purchaseItems = listOf(
38
+ // Consumables
39
+ PurchaseItem("coins_100", TYPE_IAP.PURCHASE, PurchaseCategory.CONSUMABLE),
40
+ PurchaseItem("coins_500", TYPE_IAP.PURCHASE, PurchaseCategory.CONSUMABLE),
41
+
42
+ // One-time purchases
43
+ PurchaseItem("remove_ads", TYPE_IAP.PURCHASE, PurchaseCategory.REMOVE_ADS),
44
+ PurchaseItem("lifetime_premium", TYPE_IAP.PURCHASE, PurchaseCategory.LIFETIME_PREMIUM),
45
+ PurchaseItem("unlock_levels", TYPE_IAP.PURCHASE, PurchaseCategory.FEATURE_UNLOCK),
46
+
47
+ // Subscriptions (with optional trial offer ID)
48
+ PurchaseItem("premium_monthly", "free_trial_7d", TYPE_IAP.SUBSCRIPTION),
49
+ PurchaseItem("premium_yearly", "free_trial_14d", TYPE_IAP.SUBSCRIPTION)
50
+ )
51
+
52
+ // Initialize billing
53
+ AppPurchase.getInstance().initBilling(this, purchaseItems)
54
+ }
55
+ }
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Product Configuration
61
+
62
+ ### Purchase Categories
63
+
64
+ | Category | Description | Disables Ads | Can Re-purchase |
65
+ |----------|-------------|--------------|-----------------|
66
+ | `CONSUMABLE` | Coins, gems, credits | No | Yes (after consume) |
67
+ | `FEATURE_UNLOCK` | Level packs, themes | No | No |
68
+ | `LIFETIME_PREMIUM` | Permanent premium | Yes | No |
69
+ | `REMOVE_ADS` | Ad removal only | Yes | No |
70
+
71
+ ### Creating PurchaseItems
72
+
73
+ ```kotlin
74
+ // Consumable product
75
+ PurchaseItem("coins_100", TYPE_IAP.PURCHASE, PurchaseCategory.CONSUMABLE)
76
+
77
+ // Remove ads
78
+ PurchaseItem("remove_ads", TYPE_IAP.PURCHASE, PurchaseCategory.REMOVE_ADS)
79
+
80
+ // Lifetime premium
81
+ PurchaseItem("lifetime_premium", TYPE_IAP.PURCHASE, PurchaseCategory.LIFETIME_PREMIUM)
82
+
83
+ // Feature unlock (doesn't disable ads)
84
+ PurchaseItem("unlock_themes", TYPE_IAP.PURCHASE, PurchaseCategory.FEATURE_UNLOCK)
85
+
86
+ // Subscription with free trial offer
87
+ PurchaseItem("premium_monthly", "free_trial_offer", TYPE_IAP.SUBSCRIPTION)
88
+
89
+ // Subscription without trial
90
+ PurchaseItem("premium_yearly", null, TYPE_IAP.SUBSCRIPTION)
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Making Purchases
96
+
97
+ ### In-App Products
98
+
99
+ ```kotlin
100
+ // Set purchase listener
101
+ AppPurchase.getInstance().setPurchaseListener(object : PurchaseListener {
102
+ override fun onProductPurchased(orderId: String?, originalJson: String?) {
103
+ // Purchase successful
104
+ Log.d("Billing", "Purchased! Order: $orderId")
105
+ }
106
+
107
+ override fun displayErrorMessage(errorMessage: String?) {
108
+ Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
109
+ }
110
+
111
+ override fun onUserCancelBilling() {
112
+ Log.d("Billing", "User cancelled")
113
+ }
114
+ })
115
+
116
+ // Make purchase
117
+ val result = AppPurchase.getInstance().purchase(activity, "remove_ads")
118
+ ```
119
+
120
+ ### Subscriptions
121
+
122
+ ```kotlin
123
+ // Subscribe
124
+ val result = AppPurchase.getInstance().subscribe(activity, "premium_monthly")
125
+ ```
126
+
127
+ ### Get Product Prices
128
+
129
+ ```kotlin
130
+ // Get formatted price
131
+ val price = AppPurchase.getInstance().getPrice("remove_ads") // "$2.99"
132
+ val subPrice = AppPurchase.getInstance().getPriceSub("premium_monthly") // "$9.99/month"
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Consumable Products
138
+
139
+ Consumable products require manual consumption after granting items to the user.
140
+
141
+ ### Flow
142
+
143
+ ```
144
+ Purchase → Acknowledge → onNewPurchase() → Grant Items → consumePurchase() → onPurchaseConsumed()
145
+ ```
146
+
147
+ ### Implementation
148
+
149
+ ```kotlin
150
+ AppPurchase.getInstance().setPurchaseHistoryListener(object : PurchaseHistoryListener {
151
+ override fun onNewPurchase(productId: String, purchase: PurchaseResult) {
152
+ when (productId) {
153
+ "coins_100" -> {
154
+ // Grant coins to user
155
+ userBalance += 100 * purchase.quantity
156
+ saveUserBalance()
157
+
158
+ // Now consume so they can buy again
159
+ AppPurchase.getInstance().consumePurchase(productId)
160
+ }
161
+ "coins_500" -> {
162
+ userBalance += 500 * purchase.quantity
163
+ saveUserBalance()
164
+ AppPurchase.getInstance().consumePurchase(productId)
165
+ }
166
+ }
167
+ }
168
+
169
+ override fun onPurchaseConsumed(productId: String, purchase: PurchaseResult) {
170
+ // Optional: Track in analytics
171
+ analytics.logEvent("purchase_consumed", mapOf(
172
+ "product_id" to productId,
173
+ "order_id" to purchase.orderId,
174
+ "quantity" to purchase.quantity
175
+ ))
176
+ }
177
+ })
178
+ ```
179
+
180
+ ### Why Manual Consumption?
181
+
182
+ 1. **Server verification**: Verify purchase on your server before granting items
183
+ 2. **Error handling**: If granting fails, you can retry without losing the purchase
184
+ 3. **Analytics**: Track exactly when items are granted vs purchased
185
+ 4. **Multi-quantity**: Handle purchases with quantity > 1 correctly
186
+
187
+ ---
188
+
189
+ ## Subscriptions
190
+
191
+ ### Checking Subscription Status
192
+
193
+ ```kotlin
194
+ // Check if any subscription is active
195
+ if (AppPurchase.getInstance().isSubscribed()) {
196
+ showPremiumContent()
197
+ }
198
+
199
+ // Check specific subscription
200
+ if (AppPurchase.getInstance().isSubscribed("premium_yearly")) {
201
+ showYearlyBenefits()
202
+ }
203
+
204
+ // Get subscription state
205
+ val state = AppPurchase.getInstance().getSubscriptionState("premium_monthly")
206
+ when (state) {
207
+ SubscriptionState.ACTIVE -> {
208
+ // Active and will renew
209
+ showPremiumUI()
210
+ }
211
+ SubscriptionState.CANCELLED -> {
212
+ // Cancelled but still has access until expiration
213
+ showRenewalPrompt()
214
+ }
215
+ SubscriptionState.EXPIRED -> {
216
+ // No longer subscribed
217
+ showSubscribeButton()
218
+ }
219
+ }
220
+ ```
221
+
222
+ ### Subscription State Details
223
+
224
+ | State | Has Access | Will Renew | Detection |
225
+ |-------|------------|------------|-----------|
226
+ | ACTIVE | Yes | Yes | Client-side |
227
+ | CANCELLED | Yes | No | Client-side |
228
+ | GRACE_PERIOD | Yes | Pending | Server-side only |
229
+ | ON_HOLD | No | Pending | Server-side only |
230
+ | PAUSED | No | When resumed | Server-side only |
231
+ | EXPIRED | No | No | Client-side |
232
+
233
+ ### Getting Subscription Details
234
+
235
+ ```kotlin
236
+ val subscription = AppPurchase.getInstance().getSubscription("premium_monthly")
237
+ if (subscription != null) {
238
+ println("Order ID: ${subscription.orderId}")
239
+ println("Purchase Time: ${subscription.getPurchaseTimeFormatted()}")
240
+ println("Will Renew: ${subscription.willSubscriptionRenew()}")
241
+ println("State: ${subscription.getSubscriptionStateString()}")
242
+ }
243
+
244
+ // Get all active subscriptions
245
+ val allSubs = AppPurchase.getInstance().getActiveSubscriptions()
246
+ for (sub in allSubs) {
247
+ println("${sub.getFirstProductId()}: ${sub.getSubscriptionStateString()}")
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Subscription Upgrade/Downgrade
254
+
255
+ ### Simple Methods
256
+
257
+ ```kotlin
258
+ // Upgrade to higher tier (charges price difference immediately)
259
+ AppPurchase.getInstance().upgradeSubscription(activity, "premium_yearly")
260
+
261
+ // Downgrade to lower tier (takes effect at next renewal)
262
+ AppPurchase.getInstance().downgradeSubscription(activity, "premium_basic")
263
+ ```
264
+
265
+ ### Full Control
266
+
267
+ ```kotlin
268
+ // Specify source and target subscriptions with proration mode
269
+ AppPurchase.getInstance().changeSubscription(
270
+ activity,
271
+ "premium_monthly", // Current subscription ID
272
+ "premium_yearly", // New subscription ID
273
+ SubscriptionReplacementMode.CHARGE_PRORATED_PRICE
274
+ )
275
+ ```
276
+
277
+ ### Replacement Modes
278
+
279
+ | Mode | Best For | Effect |
280
+ |------|----------|--------|
281
+ | `CHARGE_PRORATED_PRICE` | Upgrades | User pays difference immediately |
282
+ | `DEFERRED` | Downgrades | Change at next renewal date |
283
+ | `WITH_TIME_PRORATION` | Either | Immediate, remaining value credited |
284
+ | `CHARGE_FULL_PRICE` | Either | Immediate, full price charged |
285
+ | `WITHOUT_PRORATION` | Either | Immediate, no price change |
286
+
287
+ ### Example: Subscription Tier UI
288
+
289
+ ```kotlin
290
+ class SubscriptionActivity : AppCompatActivity() {
291
+
292
+ override fun onCreate(savedInstanceState: Bundle?) {
293
+ super.onCreate(savedInstanceState)
294
+
295
+ val currentSub = AppPurchase.getInstance().getActiveSubscriptions().firstOrNull()
296
+
297
+ if (currentSub == null) {
298
+ // No subscription - show all options
299
+ showAllSubscriptionOptions()
300
+ } else {
301
+ // Has subscription - show upgrade/downgrade options
302
+ val currentId = currentSub.getFirstProductId()
303
+ when (currentId) {
304
+ "premium_basic" -> showUpgradeOptions()
305
+ "premium_yearly" -> showDowngradeOptions()
306
+ else -> showAllOptions()
307
+ }
308
+ }
309
+ }
310
+
311
+ fun onUpgradeClick() {
312
+ AppPurchase.getInstance().changeSubscription(
313
+ this,
314
+ "premium_basic",
315
+ "premium_yearly",
316
+ SubscriptionReplacementMode.CHARGE_PRORATED_PRICE
317
+ )
318
+ }
319
+
320
+ fun onDowngradeClick() {
321
+ AppPurchase.getInstance().changeSubscription(
322
+ this,
323
+ "premium_yearly",
324
+ "premium_basic",
325
+ SubscriptionReplacementMode.DEFERRED
326
+ )
327
+ }
328
+ }
329
+ ```
330
+
331
+ ---
332
+
333
+ ## Purchase State Checking
334
+
335
+ ### Check If Ads Should Be Disabled
336
+
337
+ ```kotlin
338
+ // Returns true for subscriptions, lifetime premium, or remove ads
339
+ if (AppPurchase.getInstance().isPurchased()) {
340
+ hideAds()
341
+ }
342
+
343
+ // Same as isPurchased()
344
+ if (AppPurchase.getInstance().shouldDisableAds()) {
345
+ hideAds()
346
+ }
347
+ ```
348
+
349
+ ### Get Active Purchase Type
350
+
351
+ ```kotlin
352
+ when (AppPurchase.getInstance().getActivePurchaseType()) {
353
+ PurchaseType.SUBSCRIPTION -> {
354
+ showBadge("Subscriber")
355
+ }
356
+ PurchaseType.LIFETIME_PREMIUM -> {
357
+ showBadge("Lifetime Member")
358
+ }
359
+ PurchaseType.REMOVE_ADS -> {
360
+ // Just ad removal, no premium features
361
+ }
362
+ PurchaseType.NONE -> {
363
+ showPurchasePrompt()
364
+ }
365
+ }
366
+ ```
367
+
368
+ ### Check Specific Products
369
+
370
+ ```kotlin
371
+ // Check if product is owned
372
+ if (AppPurchase.getInstance().isProductOwned("unlock_themes")) {
373
+ enableThemes()
374
+ }
375
+
376
+ // Check lifetime purchases
377
+ if (AppPurchase.getInstance().hasLifetimePremium()) {
378
+ enableAllFeatures()
379
+ }
380
+
381
+ if (AppPurchase.getInstance().hasRemoveAdsPurchase()) {
382
+ hideAds()
383
+ }
384
+ ```
385
+
386
+ ---
387
+
388
+ ## Purchase History Tracking
389
+
390
+ ### Setting Up the Listener
391
+
392
+ ```kotlin
393
+ AppPurchase.getInstance().setPurchaseHistoryListener(object : PurchaseHistoryListener {
394
+ override fun onNewPurchase(productId: String, purchase: PurchaseResult) {
395
+ // Called when any purchase is acknowledged
396
+ trackPurchase(productId, purchase)
397
+ }
398
+
399
+ override fun onPurchaseConsumed(productId: String, purchase: PurchaseResult) {
400
+ // Called when consumePurchase() completes
401
+ trackConsumption(productId, purchase)
402
+ }
403
+ })
404
+ ```
405
+
406
+ ### PurchaseResult Data Available
407
+
408
+ ```kotlin
409
+ override fun onNewPurchase(productId: String, purchase: PurchaseResult) {
410
+ // Basic info
411
+ val orderId = purchase.orderId
412
+ val quantity = purchase.quantity
413
+ val purchaseTime = purchase.purchaseTime
414
+
415
+ // Formatted helpers
416
+ val dateStr = purchase.getPurchaseTimeFormatted("yyyy-MM-dd")
417
+ val stateStr = purchase.getPurchaseStateString()
418
+
419
+ // Verification data (for server)
420
+ val json = purchase.originalJson
421
+ val signature = purchase.signature
422
+
423
+ // Account identifiers
424
+ val accountId = purchase.obfuscatedAccountId
425
+ val profileId = purchase.obfuscatedProfileId
426
+
427
+ // Product type
428
+ val isSubscription = purchase.isSubscription()
429
+ val isInApp = purchase.isInApp()
430
+ }
431
+ ```
432
+
433
+ ### Persisting Purchase History
434
+
435
+ ```kotlin
436
+ class MyPurchaseTracker : PurchaseHistoryListener {
437
+
438
+ private val prefs: SharedPreferences
439
+
440
+ override fun onNewPurchase(productId: String, purchase: PurchaseResult) {
441
+ val history = getPurchaseHistory(productId).toMutableList()
442
+ history.add(PurchaseRecord(
443
+ orderId = purchase.orderId ?: "",
444
+ quantity = purchase.quantity,
445
+ purchaseTime = purchase.purchaseTime,
446
+ consumed = false
447
+ ))
448
+ savePurchaseHistory(productId, history)
449
+
450
+ // Track total purchases
451
+ val totalPurchases = prefs.getInt("total_$productId", 0)
452
+ prefs.edit().putInt("total_$productId", totalPurchases + purchase.quantity).apply()
453
+ }
454
+
455
+ override fun onPurchaseConsumed(productId: String, purchase: PurchaseResult) {
456
+ val history = getPurchaseHistory(productId).toMutableList()
457
+ history.find { it.orderId == purchase.orderId }?.consumed = true
458
+ savePurchaseHistory(productId, history)
459
+ }
460
+
461
+ fun getTotalPurchaseCount(productId: String): Int {
462
+ return prefs.getInt("total_$productId", 0)
463
+ }
464
+ }
465
+ ```
466
+
467
+ ---
468
+
469
+ ## Server-Side Verification
470
+
471
+ ### Get Verification Data
472
+
473
+ ```kotlin
474
+ AppPurchase.getInstance().setPurchaseHistoryListener(object : PurchaseHistoryListener {
475
+ override fun onNewPurchase(productId: String, purchase: PurchaseResult) {
476
+ if (purchase.hasVerificationData()) {
477
+ // Send to your server
478
+ verifyOnServer(
479
+ productId = productId,
480
+ purchaseToken = purchase.purchaseToken,
481
+ originalJson = purchase.originalJson!!,
482
+ signature = purchase.signature!!
483
+ )
484
+ }
485
+ }
486
+ })
487
+ ```
488
+
489
+ ### Server Verification Flow
490
+
491
+ ```kotlin
492
+ suspend fun verifyOnServer(
493
+ productId: String,
494
+ purchaseToken: String,
495
+ originalJson: String,
496
+ signature: String
497
+ ) {
498
+ val result = api.verifyPurchase(
499
+ productId = productId,
500
+ token = purchaseToken,
501
+ receipt = originalJson,
502
+ signature = signature
503
+ )
504
+
505
+ if (result.isValid) {
506
+ // Grant entitlement
507
+ grantProduct(productId)
508
+
509
+ // Consume if needed
510
+ if (isConsumable(productId)) {
511
+ AppPurchase.getInstance().consumePurchase(productId)
512
+ }
513
+ } else {
514
+ // Handle invalid purchase
515
+ showError("Purchase verification failed")
516
+ }
517
+ }
518
+ ```
519
+
520
+ ---
521
+
522
+ ## Subscription Expiry Verification
523
+
524
+ Google Play Billing Library does NOT provide subscription expiry dates client-side. Use server-side verification to get accurate expiry information.
525
+
526
+ ### 1. Set Up Verification Callback
527
+
528
+ ```kotlin
529
+ // In Application.onCreate()
530
+ AppPurchase.getInstance().setSubscriptionVerificationCallback { packageName, subscriptionId, purchaseToken, listener ->
531
+ // Call your backend API
532
+ yourApi.verifySubscription(packageName, subscriptionId, purchaseToken,
533
+ onSuccess = { expiryTimeMillis ->
534
+ val details = SubscriptionVerificationCallback.SubscriptionDetails.Builder()
535
+ .setExpiryTimeMillis(expiryTimeMillis)
536
+ .setAutoRenewing(true)
537
+ .build()
538
+ listener.onVerified(details)
539
+ },
540
+ onError = { error ->
541
+ listener.onVerificationFailed(error)
542
+ }
543
+ )
544
+ }
545
+ ```
546
+
547
+ ### 2. Verify Subscription
548
+
549
+ ```kotlin
550
+ AppPurchase.getInstance().verifySubscription("premium_monthly",
551
+ object : AppPurchase.SubscriptionVerificationListener {
552
+ override fun onVerified(subscription: PurchaseResult) {
553
+ // Expiry data now available
554
+ val expiryDate = subscription.getExpiryTimeFormatted("dd MMM yyyy")
555
+ val daysLeft = subscription.getRemainingDays()
556
+ val isExpired = subscription.isExpired()
557
+
558
+ Log.d("Subscription", "Expires: $expiryDate, Days left: $daysLeft")
559
+ }
560
+
561
+ override fun onVerificationFailed(errorMessage: String?) {
562
+ Log.e("Subscription", "Failed: $errorMessage")
563
+ }
564
+ }
565
+ )
566
+ ```
567
+
568
+ ### 3. Access Expiry Data
569
+
570
+ ```kotlin
571
+ // After verification, data is cached
572
+ val expiryMillis = AppPurchase.getInstance().getSubscriptionExpiryTime("premium_monthly")
573
+ val expiryFormatted = AppPurchase.getInstance().getSubscriptionExpiryTimeFormatted("premium_monthly")
574
+ val daysLeft = AppPurchase.getInstance().getSubscriptionRemainingDays("premium_monthly")
575
+ val isExpired = AppPurchase.getInstance().isSubscriptionExpired("premium_monthly")
576
+
577
+ // Or use PurchaseResult directly
578
+ val subscription = AppPurchase.getInstance().getSubscription("premium_monthly")
579
+ if (subscription?.isExpiryVerified() == true) {
580
+ val expiryDate = subscription.getExpiryDate()
581
+ val remaining = subscription.getRemainingTime()
582
+ }
583
+ ```
584
+
585
+ ### Backend API
586
+
587
+ Your server should call Google Play Developer API:
588
+
589
+ ```
590
+ GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{purchaseToken}
591
+ ```
592
+
593
+ Response includes `expiryTime`, `subscriptionState`, and more
594
+ ```
595
+
596
+ ---
597
+
598
+ ## Best Practices
599
+
600
+ ### 1. Always Handle Edge Cases
601
+
602
+ ```kotlin
603
+ AppPurchase.getInstance().setPurchaseListener(object : PurchaseListener {
604
+ override fun onProductPurchased(orderId: String?, originalJson: String?) {
605
+ // Success
606
+ }
607
+
608
+ override fun displayErrorMessage(errorMessage: String?) {
609
+ when {
610
+ errorMessage?.contains("ITEM_ALREADY_OWNED") == true -> {
611
+ // Restore purchase
612
+ AppPurchase.getInstance().verifyPurchased(true)
613
+ }
614
+ errorMessage?.contains("SERVICE_UNAVAILABLE") == true -> {
615
+ // Retry later
616
+ showRetryDialog()
617
+ }
618
+ else -> {
619
+ showError(errorMessage)
620
+ }
621
+ }
622
+ }
623
+
624
+ override fun onUserCancelBilling() {
625
+ // User cancelled - don't show error
626
+ }
627
+ })
628
+ ```
629
+
630
+ ### 2. Restore Purchases on App Start
631
+
632
+ ```kotlin
633
+ class MainActivity : AppCompatActivity() {
634
+ override fun onCreate(savedInstanceState: Bundle?) {
635
+ super.onCreate(savedInstanceState)
636
+
637
+ // Verify purchases to restore state
638
+ AppPurchase.getInstance().verifyPurchased(true)
639
+ }
640
+ }
641
+ ```
642
+
643
+ ### 3. Use Correct Categories
644
+
645
+ ```kotlin
646
+ // DON'T: Use wrong category
647
+ PurchaseItem("coins", TYPE_IAP.PURCHASE) // Defaults to LIFETIME_PREMIUM!
648
+
649
+ // DO: Explicitly set category
650
+ PurchaseItem("coins", TYPE_IAP.PURCHASE, PurchaseCategory.CONSUMABLE)
651
+ ```
652
+
653
+ ### 4. Handle Pending Purchases
654
+
655
+ ```kotlin
656
+ override fun onNewPurchase(productId: String, purchase: PurchaseResult) {
657
+ if (purchase.isPending()) {
658
+ // Don't grant yet - payment is pending
659
+ showPendingMessage("Your purchase is being processed")
660
+ return
661
+ }
662
+
663
+ if (purchase.isPurchased()) {
664
+ // Safe to grant
665
+ grantProduct(productId)
666
+ }
667
+ }
668
+ ```
669
+
670
+ ### 5. Test with Google Play Console
671
+
672
+ Use test accounts and test products:
673
+ - License testers can make test purchases
674
+ - Use static response product IDs for specific scenarios
675
+ - Test all proration modes for subscriptions
676
+
677
+ ---
678
+
679
+ ## Troubleshooting
680
+
681
+ ### "You already own this item"
682
+
683
+ The product wasn't consumed. Call `consumePurchase(productId)` after granting items.
684
+
685
+ ### Subscription not showing as active
686
+
687
+ Call `verifyPurchased(true)` to refresh purchase state from Google.
688
+
689
+ ### Purchase callback not firing
690
+
691
+ Ensure `setPurchaseListener()` is called before `purchase()`.
692
+
693
+ ### Price not available
694
+
695
+ Products may not be loaded yet. Check `getPrice()` returns non-empty string before showing UI.
696
+
697
+ ```kotlin
698
+ val price = AppPurchase.getInstance().getPrice("product_id")
699
+ if (price.isNotEmpty()) {
700
+ button.text = "Buy for $price"
701
+ } else {
702
+ button.text = "Buy"
703
+ }
704
+ ```