com.amanotes.gdk 0.2.48 → 0.2.50

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 (40) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/Editor/AmaGDKEditor.cs +149 -63
  3. package/Editor/AmaGDKExtra.cs +31 -0
  4. package/Editor/AmaGDKExtra.cs.meta +11 -0
  5. package/Editor/Extra/GDKAutoBuild.cs +891 -0
  6. package/Editor/Extra/GDKAutoBuild.cs.meta +3 -0
  7. package/Editor/Extra/GDKProjectCP.cs +158 -0
  8. package/Editor/Extra/GDKProjectCP.cs.meta +11 -0
  9. package/Editor/Extra.meta +8 -0
  10. package/Editor/Utils/AmaGDKEditor.ExtractVersion.cs +1 -1
  11. package/Extra/AmaGDKInstaller.unitypackage +0 -0
  12. package/Extra/AndroidPostProcessor.unitypackage +0 -0
  13. package/Extra/{AmaGDKProject.unitypackage.meta → AndroidPostProcessor.unitypackage.meta} +1 -1
  14. package/Extra/CheckDiskSpace.unitypackage +0 -0
  15. package/Extra/CheckDiskSpace.unitypackage.meta +7 -0
  16. package/Extra/SDKVersionTracking.unitypackage +0 -0
  17. package/Extra/SDKVersionTracking.unitypackage.meta +7 -0
  18. package/Packages/AmaGDKConfig.unitypackage +0 -0
  19. package/Packages/AmaGDKExample.unitypackage +0 -0
  20. package/Packages/AmaGDKTest.unitypackage +0 -0
  21. package/Packages/AppsFlyerAdapter.unitypackage +0 -0
  22. package/Packages/FirebaseAnalyticsAdapter.unitypackage +0 -0
  23. package/Packages/FirebaseRemoteConfigAdapter.unitypackage +0 -0
  24. package/Packages/IronSourceAdapter.unitypackage +0 -0
  25. package/Packages/RevenueCatAdapter.unitypackage +0 -0
  26. package/Runtime/AmaGDK.Adapters.cs +0 -4
  27. package/Runtime/AmaGDK.Ads.cs +92 -31
  28. package/Runtime/AmaGDK.Analytics.cs +97 -64
  29. package/Runtime/AmaGDK.IAP.cs +39 -45
  30. package/Runtime/AmaGDK.RemoteConfig.cs +76 -52
  31. package/Runtime/AmaGDK.UserProfile.cs +106 -124
  32. package/Runtime/AmaGDK.cs +46 -35
  33. package/Runtime/Internal/AmaGDK.Event.cs +185 -0
  34. package/Runtime/Internal/AmaGDK.Event.cs.meta +3 -0
  35. package/Runtime/Internal/AmaGDK.Internal.cs +4 -0
  36. package/Runtime/Internal/AmaGDK.Utils.cs +85 -24
  37. package/Runtime/Klavar/Attributes.meta +0 -4
  38. package/Runtime/Klavar.meta +0 -4
  39. package/package.json +1 -1
  40. package/Extra/AmaGDKProject.unitypackage +0 -0
@@ -26,6 +26,7 @@ namespace Amanotes.Core
26
26
  public string funnelSignature;
27
27
  public bool accumulated;
28
28
  public bool withinSession;
29
+ public bool doNotIncreaseCounter;
29
30
 
30
31
  public bool overrideConfig;
31
32
  public HashSet<string> ignoreAdapterIds = new HashSet<string>();
@@ -241,11 +242,15 @@ namespace Amanotes.Core
241
242
  internal static readonly List<AnalyticsAdapter> listAdapters = new List<AnalyticsAdapter>();
242
243
  internal static readonly AnalyticsData localData = new AnalyticsData().LoadJsonFromFile();
243
244
  internal static readonly SessionStat sessionStat = new SessionStat();
244
- internal static AnalyticsAdapter primaryAdapter;
245
- internal static readonly HashSet<string> blackListPrefixes = new HashSet<string>() {
246
- "firebase_", "google_", "ga_"
245
+
246
+ internal static readonly Dictionary<string, string> reservedPrefixes = new Dictionary<string, string>()
247
+ {
248
+ { "firebase_", "frb_" },
249
+ { "google_", "ggl_" },
250
+ { "ga_", "gga_" }
247
251
  };
248
- internal static readonly HashSet<string> blackListEvents = new HashSet<string>()
252
+
253
+ internal static readonly HashSet<string> reservedEvents = new HashSet<string>()
249
254
  {
250
255
  "ad_activeview",
251
256
  "ad_click",
@@ -284,8 +289,6 @@ namespace Amanotes.Core
284
289
  {
285
290
  listAdapters.Clear();
286
291
  listAdapters.AddRange(Adapter2.GetAllAdapter<AnalyticsAdapter>());
287
- if (listAdapters.Count != 0)
288
- primaryAdapter = listAdapters.Find(a => a.adapterId == AdapterID.FIREBASE_ANALYTICS) ?? listAdapters[0];
289
292
  }
290
293
 
291
294
  internal static void StartModule()
@@ -320,19 +323,34 @@ namespace Amanotes.Core
320
323
  UpdateAccumulatedCount(eventData, countInSession, totalCount);
321
324
  }
322
325
 
323
- eventData.AddParam("ama_device_id", User.AmaDeviceId);
326
+ if (!eventData.parameters.ContainsKey("ama_device_id"))
327
+ eventData.AddParam("ama_device_id", User.AmaDeviceId);
324
328
 
325
329
  string eventNameToSend = GetEventNameToSend(eventData.eventName, countInSession, totalCount);
326
330
  if (config.normalizeEventName) NormalizeEventName(ref eventNameToSend);
327
331
 
328
- if (showAnalyticsLog) Debug.Log($"AmaGDK | [Analytics] Event <{eventNameToSend}> (in session: {countInSession}, total: {totalCount})\n{JsonUtils.DictionaryToJson(eventData.parameters, true)}");
332
+ if (showAnalyticsLog)
333
+ {
334
+ Debug.Log($"AmaGDK [Analytics] Event <{eventNameToSend}> (in session: {countInSession}, total: {totalCount})\n{JsonUtils.DictionaryToJson(eventData.parameters, true)}");
335
+ }
329
336
 
330
337
  config.quality.CheckEventQuality(eventData.eventName, eventData.parameters);
331
338
 
332
339
  var inUseAdapterIDs = new List<string>();
333
340
  foreach (AnalyticsAdapter m in listAdapters)
334
341
  {
335
- if (IsAdapterForbidden(m, eventData)) continue;
342
+ if (eventData.overrideConfig)
343
+ {
344
+ // IMPORTANT NOTE:
345
+ //
346
+ // Once set overrideConfig, we should skip the adapter config completely!!!
347
+ //
348
+ if (eventData.IsAdapterForbidden(m.adapterId)) continue;
349
+ }
350
+ else // Do not remove else
351
+ {
352
+ if (m.IsForbidden(eventData.eventName)) continue;
353
+ }
336
354
  m.LogEvent(eventNameToSend, eventData.parameters);
337
355
  inUseAdapterIDs.Add(m.adapterId);
338
356
  }
@@ -356,35 +374,18 @@ namespace Amanotes.Core
356
374
  }
357
375
 
358
376
  int.TryParse(eventData.parameters[ACCUMULATED_COUNT].ToString(), out int oriAccCount);
359
- if (oriAccCount == countByGDK)
360
- {
361
- eventData.AddParam(ACCUMULATED_COUNT, countByGDK);
362
- }
363
- else
377
+ if (oriAccCount != countByGDK)
364
378
  {
365
379
  LogWarning($"[Analytics] event <{eventData.eventName}> has difference in accumulated count. Original count: {oriAccCount}. AmaGDK count: {countByGDK}");
380
+ if (Config.analytics.migrateAccumulatedCount && !eventData.withinSession)
381
+ {
382
+ Log($"[Analytics] Update event count of event <{eventData.eventName}> from {countByGDK} to {oriAccCount}");
383
+ eventData.EventDetail.totalCount = oriAccCount;
384
+ localData._dirty = true;
385
+ }
366
386
  }
367
387
  }
368
388
 
369
- internal static bool IsAdapterForbidden(AnalyticsAdapter adapter, EventParams eventData)
370
- {
371
- if (adapter == null) return false;
372
- if (eventData.overrideConfig)
373
- {
374
- // IMPORTANT NOTE:
375
- //
376
- // Once set overrideConfig, we should skip the adapter config completely!!!
377
- //
378
- if (eventData.IsAdapterForbidden(adapter.adapterId)) return true;
379
- }
380
- else // Do not remove else
381
- {
382
- if (adapter.IsForbidden(eventData.eventName)) return true;
383
- }
384
-
385
- return false;
386
- }
387
-
388
389
  internal static string GetEventNameToSend(string eventName, int countInSession, int totalCount)
389
390
  {
390
391
  if (!sendAtCount.TryGetValue(eventName, out var config)) return eventName;
@@ -401,14 +402,14 @@ namespace Amanotes.Core
401
402
  StringBuilder sb = tmpWarningSB;
402
403
  sb.Clear();
403
404
  NormalizeKeyString(ref eventName, sb, dryRun);
404
- if (blackListEvents.Contains(eventName))
405
+ if (reservedEvents.Contains(eventName))
405
406
  sb.AppendLine($" - EventName <{eventName}> is reserved and should not be used");
406
407
  if (sb.Length > 0 && dryRun) LogWarningOnce($"Warning for event name <{eventName}>:\n{sb}", $"NormalizeEventName<{eventName}>");
407
408
  }
408
409
  Profiler.EndSample();
409
410
  }
410
411
 
411
- internal static void NormalizeKeyString(ref string key, StringBuilder warningLog, bool dryRun = false)
412
+ internal static void NormalizeKeyString(ref string key, StringBuilder warningLog, bool dryRun)
412
413
  {
413
414
  Profiler.BeginSample("AmaGDK.Analytics.NormalizeEventName()");
414
415
  {
@@ -418,47 +419,53 @@ namespace Amanotes.Core
418
419
  Profiler.EndSample();
419
420
  return;
420
421
  }
422
+
423
+ var sb = new StringBuilder();
421
424
 
422
425
  string normalizedString = key.Trim()
423
426
  .Replace(' ', '_')
424
427
  .Replace('-', '_');
425
-
426
428
  if (key != normalizedString)
427
429
  {
428
- if (!dryRun)
429
- {
430
- key = normalizedString;
431
- } else
432
- {
433
- warningLog.AppendLine($" - Malformed name (should not contains <space> or <dash>): <{key}>");
434
- }
430
+ sb.Append($" - Malformed name (should not contains <space> or <dash>): <{key}>");
435
431
  }
436
432
 
437
433
  var r = new Regex("^[a-zA-Z0-9_]*$");
438
434
  if (!r.IsMatch(normalizedString))
439
435
  {
440
- warningLog.AppendLine($" - Name may only contain alphanumeric characters and underscores");
436
+ normalizedString = Regex.Replace(normalizedString, "[^a-zA-Z0-9_]", "");
437
+ sb.Append(" - Name may only contain alphanumeric characters and underscores");
441
438
  }
442
439
 
443
- if (!char.IsLetter(key[0]))
440
+ if (!char.IsLetter(normalizedString[0]))
444
441
  {
445
- warningLog.AppendLine($" - Name must start with a letter: <{key}> ");
442
+ normalizedString = "E" + normalizedString;
443
+ sb.Append($" - Name must start with a letter: <{key}> ");
446
444
  }
447
445
 
448
- foreach (var prefix in blackListPrefixes)
446
+ foreach (var prefix in reservedPrefixes)
449
447
  {
450
- if (!normalizedString.StartsWith(prefix)) continue;
451
- warningLog.AppendLine($" - <{key}> starts with reserved prefix <{prefix}>");
448
+ if (!normalizedString.StartsWith(prefix.Key)) continue;
449
+ normalizedString = prefix.Value + normalizedString.Remove(0, prefix.Key.Length);
450
+ sb.AppendLine($" - <{key}> starts with reserved prefix <{prefix}>");
452
451
  }
453
452
 
454
- if (key.Length > 40)
453
+ if (normalizedString.Length > 40)
454
+ {
455
+ sb.AppendLine($" - Name is too long ({key.Length} > 40): <{key}> ");
456
+ normalizedString = normalizedString.Substring(0, 40);
457
+ }
458
+
459
+ if (dryRun)
455
460
  {
456
- warningLog.AppendLine($" - Name is too long ({key.Length} > 40): <{key}> ");
461
+ warningLog.Append(sb);
462
+ } else {
463
+ key = normalizedString;
457
464
  }
458
465
  }
459
466
  Profiler.EndSample();
460
467
  }
461
-
468
+
462
469
  internal static bool ShouldSendEvent(EventParams eventData)
463
470
  {
464
471
  //Funnel event
@@ -609,8 +616,7 @@ namespace Amanotes.Core
609
616
  internal (int countInSession, int totalCount) IncreaseCounter(EventParams eventParams)
610
617
  {
611
618
  EventDetail result = GetEventDetail(eventParams.eventName, true);
612
- if (IsAdapterForbidden(primaryAdapter, eventParams))
613
- return (result.countInSession, result.totalCount);
619
+ if (eventParams.doNotIncreaseCounter) return (result.countInSession, result.totalCount);
614
620
  result.countInSession++;
615
621
  result.totalCount++;
616
622
  _dirty = true;
@@ -708,6 +714,15 @@ namespace Amanotes.Core
708
714
  return ap;
709
715
  }
710
716
 
717
+ public static IEventParamsBuilder SkipAccumulatedCount(this IEventParamsBuilder ap)
718
+ {
719
+ if (ap == null) return null;
720
+
721
+ var data = ap as EventParams;
722
+ data!.doNotIncreaseCounter = true;
723
+ return ap;
724
+ }
725
+
711
726
  public static IEventParamsBuilder Except(this IEventParamsBuilder ap, params string[] analyticIds)
712
727
  {
713
728
  if (ap == null) return null;
@@ -759,7 +774,7 @@ namespace Amanotes.Core
759
774
  public static IEventParamsBuilder AddParam(this IEventParamsBuilder ap, string key, object value)
760
775
  {
761
776
  if (ap == null) return null;
762
- if (!IsValidKeyValue(ap, key, value)) return ap;
777
+ if (!IsValidKeyValue(ap, ref key, ref value)) return ap;
763
778
 
764
779
  var eventParams = ap as EventParams;
765
780
  Dictionary<string, object> parameters = eventParams?.parameters;
@@ -789,10 +804,12 @@ namespace Amanotes.Core
789
804
  return ap;
790
805
  }
791
806
 
792
- foreach (KeyValuePair<string, object> kvp in dictParams)
807
+ foreach (var kvp in dictParams)
793
808
  {
794
- if (IsValidKeyValue(ap, kvp.Key, kvp.Value))
795
- ap.AddParam(kvp.Key, kvp.Value);
809
+ var key = kvp.Key;
810
+ var value = kvp.Value;
811
+ if (IsValidKeyValue(ap, ref key, ref value))
812
+ ap.AddParam(key, value);
796
813
  }
797
814
  return ap;
798
815
  }
@@ -808,33 +825,48 @@ namespace Amanotes.Core
808
825
 
809
826
  foreach (var kvp in parameters)
810
827
  {
811
- if (IsValidKeyValue(ap, kvp.key, kvp.value))
812
- ap.AddParam(kvp.key, kvp.value);
828
+ var key = kvp.key;
829
+ var value = kvp.value;
830
+ if (IsValidKeyValue(ap, ref key, ref value))
831
+ ap.AddParam(key, value);
813
832
  }
814
833
  return ap;
815
834
  }
816
835
 
817
836
  //https://firebase.google.com/docs/reference/android/com/google/firebase/analytics/FirebaseAnalytics.Param
818
- private static bool IsValidKeyValue(IEventParamsBuilder ap, string key, object value)
837
+ private static bool IsValidKeyValue(IEventParamsBuilder ap, ref string key, ref object value)
819
838
  {
820
839
  string eventName = (ap as EventParams)?.eventName;
821
840
 
822
841
  StringBuilder sb = tmpWarningSB;
823
842
  sb.Clear();
824
- NormalizeKeyString(ref key, sb);
825
- if (sb.Length > 0) LogWarningOnce($"Warning for event <{eventName}>, param <{key}>:\n{sb}", $"NormalizeParamKey<{eventName}>");
843
+ NormalizeKeyString(ref key, sb, AmaGDK.Config.analytics.normalizeEventName);
844
+
845
+ if (sb.Length > 0) LogWarningOnce($"Warning for event <{eventName}>, param <{key}>:\n{sb}", $"NormalizeParamKey <{eventName}>");
826
846
 
827
847
  if (string.IsNullOrWhiteSpace(key))
828
848
  {
829
849
  LogWarning($"Param key is empty. Event name: {eventName}!");
830
850
  return false;
831
851
  }
852
+
832
853
  if (value == null)
833
854
  {
834
- LogWarning($"Param value of key <{key}> is null. Event name: {eventName}!");
855
+ LogWarning($"Param value is null. Event name: {eventName}, key: {key}!");
835
856
  return false;
836
857
  }
837
858
 
859
+ if (value.GetType() == typeof(string))
860
+ {
861
+ var v = value.ToString();
862
+ var strLimit = 100;
863
+ if (v.Length > strLimit)
864
+ {
865
+ value = v.Substring(0, strLimit);
866
+ LogWarning($"Param value exceeds the length limit ({strLimit}). Event name: {eventName}, key: {key}!");
867
+ }
868
+ }
869
+
838
870
  return true;
839
871
  }
840
872
  }
@@ -848,6 +880,7 @@ namespace Amanotes.Core.Internal
848
880
  public bool showAnalyticsLog = true;
849
881
  public bool normalizeEventName = true;
850
882
  public bool checkMigrationIntegrity = true;
883
+ public bool migrateAccumulatedCount = true;
851
884
  public bool enableEventStat;
852
885
  public AnalyticQualityConfig quality;
853
886
  }
@@ -20,7 +20,7 @@ namespace Amanotes.Core
20
20
  public static DateTime? SubscriptionLastPurchaseDate => localData.SubscriptionLastPurchaseDate;
21
21
  public static DateTime? SubscriptionExpiredDate => localData.SubscriptionExpiredDate;
22
22
  public static DateTime? UnsubscribedDate => localData.UnsubscribedDate;
23
- public static Dictionary<string, ProductInfo> AllProducts => dicProducts;
23
+ public static readonly Dictionary<string, ProductInfo> AllProducts = new Dictionary<string, ProductInfo>();
24
24
  private const string DATE_FORMAT = "yyyy/MM/yy HH:mm:ss";
25
25
 
26
26
  public static ISetupBuilder GetProducts(bool appendExistingList = false)
@@ -28,42 +28,62 @@ namespace Amanotes.Core
28
28
  if (!appendExistingList)
29
29
  {
30
30
  IAPExtension.isCleanUpConfig = false;
31
- dicProducts.Clear();
31
+ AllProducts.Clear();
32
32
  }
33
33
  var result = new SetupContext();
34
- ExecuteInNextFrame(result.GetProducts);
34
+ GDKUtils.DelayCall(0f, result.GetProducts);
35
35
  return result;
36
36
  }
37
37
 
38
38
  public static IPurchaseBuilder Purchase(string productId)
39
39
  {
40
40
  var result = new PurchaseContext();
41
- if (!dicProducts.ContainsKey(productId))
41
+ GDKUtils.DelayCall(0f, () =>
42
42
  {
43
- LogWarning($"[IAP] Product {productId} is not found");
44
- result.onFail?.Invoke(new PurchaseError($"Product {productId} is not found"));
45
- return result;
46
- }
47
- ExecuteInNextFrame(() => result.Purchase(dicProducts[productId]));
43
+ if (!AllProducts.ContainsKey(productId))
44
+ {
45
+ LogWarning($"[IAP] Product {productId} is not found");
46
+ result.onFail?.Invoke(new PurchaseError($"Product {productId} is not found"));
47
+ return;
48
+ }
49
+
50
+ #if UNITY_EDITOR
51
+ if (Config.iap.allPurchaseSuccessInEditor)
52
+ {
53
+ var s = new PurchaseSuccess
54
+ {
55
+ productIds = new[] { productId },
56
+ isSandbox = true
57
+ };
58
+ result.onSuccess?.Invoke(s);
59
+ }
60
+ else
61
+ {
62
+ result.onFail?.Invoke(new PurchaseError("Force to fail", productId));
63
+ }
64
+ #else
65
+ result.Purchase(AllProducts[productId]);
66
+ #endif
67
+ });
48
68
  return result;
49
69
  }
50
70
 
51
71
  public static IPurchaseBuilder RestorePurchase()
52
72
  {
53
73
  var result = new PurchaseContext();
54
- ExecuteInNextFrame(result.RestorePurchase);
74
+ GDKUtils.DelayCall(0f, result.RestorePurchase);
55
75
  return result;
56
76
  }
57
77
 
58
78
  public static ProductInfo GetProduct(string productId)
59
79
  {
60
- if (!dicProducts.ContainsKey(productId))
80
+ if (!AllProducts.ContainsKey(productId))
61
81
  {
62
82
  LogWarning($"[IAP] Not found product with id <{productId}>. Make sure you have called GetProducts() first.");
63
83
  return null;
64
84
  }
65
85
 
66
- return dicProducts[productId];
86
+ return AllProducts[productId];
67
87
  }
68
88
 
69
89
  public static List<string> GetActiveSubscriptions()
@@ -108,10 +128,7 @@ namespace Amanotes.Core
108
128
  return null;
109
129
  }
110
130
  }
111
-
112
131
 
113
- internal static Dictionary<string, ProductInfo> dicProducts = new Dictionary<string, ProductInfo>();
114
-
115
132
  internal static bool InitModule()
116
133
  {
117
134
  _adapter = Adapter2.GetAdapter<IAPAdapter>();
@@ -126,17 +143,6 @@ namespace Amanotes.Core
126
143
  localData.Sync();
127
144
  if (Config.iap.autoGetProducts) GetProducts();
128
145
  }
129
-
130
- internal static void ExecuteInNextFrame(Action method)
131
- {
132
- _instance.StartCoroutine(ExecuteRoutine(method));
133
- }
134
-
135
- static IEnumerator ExecuteRoutine(Action method)
136
- {
137
- yield return null;
138
- method?.Invoke();
139
- }
140
146
  }
141
147
 
142
148
  public partial class IAP //Persistent
@@ -431,10 +437,10 @@ namespace Amanotes.Core.Internal
431
437
  #if UNITY_EDITOR
432
438
  foreach (var p in ls)
433
439
  {
434
- if (dicProducts.ContainsKey(p.id)) dicProducts[p.id] = p;
435
- else dicProducts.Add(p.id, p);
440
+ if (AllProducts.ContainsKey(p.id)) AllProducts[p.id] = p;
441
+ else AllProducts.Add(p.id, p);
436
442
  }
437
- onSuccess?.Invoke(dicProducts);
443
+ onSuccess?.Invoke(AllProducts);
438
444
  return;
439
445
  #else
440
446
 
@@ -473,7 +479,7 @@ namespace Amanotes.Core.Internal
473
479
  {
474
480
  if (getProdQueue.Count <= 0)
475
481
  {
476
- onSuccess?.Invoke(dicProducts);
482
+ onSuccess?.Invoke(AllProducts);
477
483
  return;
478
484
  }
479
485
 
@@ -489,10 +495,10 @@ namespace Amanotes.Core.Internal
489
495
  {
490
496
  foreach (var p in pList)
491
497
  {
492
- if (dicProducts.ContainsKey(p.id)) dicProducts[p.id] = p;
493
- else dicProducts.Add(p.id, p);
498
+ if (AllProducts.ContainsKey(p.id)) AllProducts[p.id] = p;
499
+ else AllProducts.Add(p.id, p);
494
500
  }
495
- ExecuteInNextFrame(GetNextProductsInQueue);
501
+ GDKUtils.DelayCall(0f, GetNextProductsInQueue);
496
502
  }, error => onFail?.Invoke(error));
497
503
  }
498
504
  catch (Exception e)
@@ -513,18 +519,6 @@ namespace Amanotes.Core.Internal
513
519
 
514
520
  public void Purchase(ProductInfo productInfo)
515
521
  {
516
- #if UNITY_EDITOR
517
- if (AmaGDK.Config.iap.allPurchaseSuccessInEditor)
518
- {
519
- DoOnSuccess(new PurchaseSuccess
520
- {
521
- productIds = new[] { productInfo.id },
522
- isSandbox = true
523
- });
524
- return;
525
- }
526
- #endif
527
-
528
522
  if (!string.IsNullOrWhiteSpace(discountId))
529
523
  {
530
524
  AmaGDK.IAP.Adapter?.PurchaseDiscount(productInfo, discountId, DoOnSuccess, DoOnFail);