react-native-purchases-ui 9.2.3 → 9.4.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 (32) hide show
  1. package/RNPaywalls.podspec +1 -1
  2. package/android/build.gradle +3 -3
  3. package/android/src/main/java/com/revenuecat/purchases/react/ui/BasePaywallViewManager.kt +25 -5
  4. package/android/src/main/java/com/revenuecat/purchases/react/ui/PaywallFooterViewManager.kt +7 -2
  5. package/android/src/main/java/com/revenuecat/purchases/react/ui/PaywallViewManager.kt +7 -2
  6. package/android/src/main/java/com/revenuecat/purchases/react/ui/RNCustomerCenterModule.kt +1 -2
  7. package/android/src/main/java/com/revenuecat/purchases/react/ui/RNPaywallsModule.kt +12 -3
  8. package/android/src/main/java/com/revenuecat/purchases/react/ui/RNPurchasesConverters.kt +27 -0
  9. package/android/src/main/java/com/revenuecat/purchases/react/ui/customercenter/events/CustomerCenterEventName.kt +2 -1
  10. package/android/src/main/java/com/revenuecat/purchases/react/ui/views/WrappedPaywallComposeView.kt +3 -2
  11. package/android/src/main/java/com/revenuecat/purchases/react/ui/views/WrappedPaywallFooterComposeView.kt +12 -2
  12. package/ios/CustomerCenterViewManager.m +6 -0
  13. package/ios/CustomerCenterViewWrapper.h +2 -0
  14. package/ios/CustomerCenterViewWrapper.m +24 -1
  15. package/ios/PaywallViewWrapper.m +45 -1
  16. package/ios/RNCustomerCenter.m +9 -0
  17. package/ios/RNPaywalls.m +8 -0
  18. package/lib/commonjs/index.js +35 -12
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/preview/previewComponents.js +35 -1
  21. package/lib/commonjs/preview/previewComponents.js.map +1 -1
  22. package/lib/module/index.js +36 -13
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/preview/previewComponents.js +33 -0
  25. package/lib/module/preview/previewComponents.js.map +1 -1
  26. package/lib/typescript/src/index.d.ts +19 -0
  27. package/lib/typescript/src/index.d.ts.map +1 -1
  28. package/lib/typescript/src/preview/previewComponents.d.ts +6 -0
  29. package/lib/typescript/src/preview/previewComponents.d.ts.map +1 -1
  30. package/package.json +3 -3
  31. package/src/index.tsx +57 -12
  32. package/src/preview/previewComponents.tsx +47 -2
@@ -17,6 +17,6 @@ Pod::Spec.new do |spec|
17
17
  spec.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
18
18
 
19
19
  spec.dependency "React-Core"
20
- spec.dependency "PurchasesHybridCommonUI", '17.0.0'
20
+ spec.dependency "PurchasesHybridCommonUI", '17.5.1'
21
21
  spec.swift_version = '5.7'
22
22
  end
@@ -8,7 +8,7 @@ buildscript {
8
8
  }
9
9
 
10
10
  dependencies {
11
- classpath "com.android.tools.build:gradle:8.12.1"
11
+ classpath "com.android.tools.build:gradle:8.13.0"
12
12
  // noinspection DifferentKotlinGradleVersion
13
13
  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14
14
  }
@@ -59,7 +59,7 @@ android {
59
59
  minSdkVersion getExtOrIntegerDefault("minSdkVersion")
60
60
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
61
61
  versionCode 1
62
- versionName '9.2.3'
62
+ versionName '9.4.0'
63
63
  }
64
64
 
65
65
  buildTypes {
@@ -91,7 +91,7 @@ dependencies {
91
91
  //noinspection GradleDynamicVersion
92
92
  implementation "com.facebook.react:react-native:+"
93
93
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
94
- implementation 'com.revenuecat.purchases:purchases-hybrid-common-ui:17.0.0'
94
+ implementation 'com.revenuecat.purchases:purchases-hybrid-common-ui:17.5.1'
95
95
  implementation 'androidx.compose.ui:ui-android:1.5.4'
96
96
  implementation "androidx.appcompat:appcompat:1.6.1"
97
97
  }
@@ -10,6 +10,8 @@ import com.facebook.react.uimanager.ThemedReactContext
10
10
  import com.facebook.react.uimanager.UIManagerHelper
11
11
  import com.facebook.react.uimanager.annotations.ReactProp
12
12
  import com.facebook.react.uimanager.events.Event
13
+ import com.revenuecat.purchases.Package
14
+ import com.revenuecat.purchases.PresentedOfferingContext
13
15
  import com.revenuecat.purchases.hybridcommon.ui.PaywallListenerWrapper
14
16
  import com.revenuecat.purchases.react.ui.events.OnDismissEvent
15
17
  import com.revenuecat.purchases.react.ui.events.OnPurchaseCancelledEvent
@@ -29,9 +31,13 @@ internal abstract class BasePaywallViewManager<T : View> : SimpleViewManager<T>(
29
31
  private const val OFFERING_IDENTIFIER = "identifier"
30
32
  private const val OPTION_FONT_FAMILY = "fontFamily"
31
33
  private const val OPTION_DISPLAY_CLOSE_BUTTON = "displayCloseButton"
34
+
35
+ private const val OPTION_OFFERING_AVAILABLE_PACKAGES = "availablePackages"
36
+
37
+ private const val OPTION_OFFERING_AVAILABLE_PACKAGES_PRESENTED_OFFERING_CONTEXT = "presentedOfferingContext"
32
38
  }
33
39
 
34
- abstract fun setOfferingId(view: T, identifier: String)
40
+ abstract fun setOfferingId(view: T, offeringId: String?, presentedOfferingContext: PresentedOfferingContext? = null)
35
41
 
36
42
  abstract fun setDisplayDismissButton(view: T, display: Boolean)
37
43
 
@@ -69,12 +75,26 @@ internal abstract class BasePaywallViewManager<T : View> : SimpleViewManager<T>(
69
75
  // getDynamic crashes if the value is null, that's why we use props?.toHashMap
70
76
  return
71
77
  }
78
+
79
+ val offeringMap = options.getDynamic(OPTION_OFFERING).asMap();
80
+
72
81
  // this is a workaround for the fact that getDynamic doesn't work with null values
73
- val offeringIdentifier =
74
- options.getDynamic(OPTION_OFFERING)?.asMap()?.getString(OFFERING_IDENTIFIER)
75
- offeringIdentifier?.let {
76
- setOfferingId(view, it)
82
+ val offeringIdentifier = offeringMap.getString(OFFERING_IDENTIFIER)
83
+ if (offeringIdentifier == null) {
84
+ return
77
85
  }
86
+
87
+ val presentedOfferingContext = getPresentedOfferingContext(offeringIdentifier, offeringMap)
88
+
89
+ setOfferingId(view, offeringIdentifier, presentedOfferingContext)
90
+ }
91
+
92
+ private fun getPresentedOfferingContext(offeringIdentifier: String, offeringMap: ReadableMap?) : PresentedOfferingContext {
93
+ val availablePackages = offeringMap?.getArray(OPTION_OFFERING_AVAILABLE_PACKAGES)?.toArrayList()
94
+ val firstAvailablePackage = availablePackages?.firstOrNull() as? Map<*, *>;
95
+ val presentedOfferingContextMap = firstAvailablePackage?.get(OPTION_OFFERING_AVAILABLE_PACKAGES_PRESENTED_OFFERING_CONTEXT) as? Map<*,*>;
96
+
97
+ return RNPurchasesConverters.presentedOfferingContext(offeringIdentifier, presentedOfferingContextMap)
78
98
  }
79
99
 
80
100
  private fun setFontFamilyProp(view: T, options: ReadableMap?) {
@@ -2,6 +2,7 @@ package com.revenuecat.purchases.react.ui
2
2
 
3
3
  import androidx.core.view.children
4
4
  import com.facebook.react.uimanager.ThemedReactContext
5
+ import com.revenuecat.purchases.PresentedOfferingContext
5
6
  import com.revenuecat.purchases.react.ui.events.OnMeasureEvent
6
7
  import com.revenuecat.purchases.react.ui.views.WrappedPaywallFooterComposeView
7
8
  import com.revenuecat.purchases.ui.revenuecatui.fonts.CustomFontProvider
@@ -72,8 +73,12 @@ internal class PaywallFooterViewManager : BasePaywallViewManager<WrappedPaywallF
72
73
  }
73
74
  }
74
75
 
75
- override fun setOfferingId(view: WrappedPaywallFooterComposeView, identifier: String) {
76
- view.setOfferingId(identifier)
76
+ override fun setOfferingId(
77
+ view: WrappedPaywallFooterComposeView,
78
+ offeringId: String?,
79
+ presentedOfferingContext: PresentedOfferingContext?
80
+ ) {
81
+ view.setOfferingId(offeringId, presentedOfferingContext)
77
82
  }
78
83
 
79
84
  override fun setFontFamily(view: WrappedPaywallFooterComposeView, customFontProvider: CustomFontProvider) {
@@ -1,6 +1,7 @@
1
1
  package com.revenuecat.purchases.react.ui
2
2
 
3
3
  import com.facebook.react.uimanager.ThemedReactContext
4
+ import com.revenuecat.purchases.PresentedOfferingContext
4
5
  import com.revenuecat.purchases.react.ui.views.WrappedPaywallComposeView
5
6
  import com.revenuecat.purchases.ui.revenuecatui.fonts.CustomFontProvider
6
7
 
@@ -26,8 +27,12 @@ internal class PaywallViewManager : BasePaywallViewManager<WrappedPaywallCompose
26
27
  return PaywallViewShadowNode()
27
28
  }
28
29
 
29
- override fun setOfferingId(view: WrappedPaywallComposeView, identifier: String) {
30
- view.setOfferingId(identifier)
30
+ override fun setOfferingId(
31
+ view: WrappedPaywallComposeView,
32
+ offeringId: String?,
33
+ presentedOfferingContext: PresentedOfferingContext?
34
+ ) {
35
+ view.setOfferingId(offeringId, presentedOfferingContext)
31
36
  }
32
37
 
33
38
  override fun setFontFamily(view: WrappedPaywallComposeView, customFontProvider: CustomFontProvider) {
@@ -38,10 +38,8 @@ internal class RNCustomerCenterModule(
38
38
  ) {
39
39
  if (requestCode == REQUEST_CODE_CUSTOMER_CENTER) {
40
40
  if (resultCode == Activity.RESULT_OK) {
41
- Log.d(NAME, "Customer Center closed successfully")
42
41
  customerCenterPromise?.resolve(null)
43
42
  } else {
44
- Log.d(NAME, "Customer Center closed with result $resultCode")
45
43
  customerCenterPromise?.reject(
46
44
  "CUSTOMER_CENTER_ERROR",
47
45
  "Customer Center closed with result code: $resultCode",
@@ -99,6 +97,7 @@ internal class RNCustomerCenterModule(
99
97
 
100
98
  private fun createCustomerCenterListener(): CustomerCenterListener {
101
99
  return object : CustomerCenterListenerWrapper() {
100
+
102
101
  override fun onFeedbackSurveyCompletedWrapper(feedbackSurveyOptionId: String) {
103
102
  val params = WritableNativeMap().apply {
104
103
  putString("feedbackSurveyOptionId", feedbackSurveyOptionId)
@@ -6,6 +6,7 @@ import com.facebook.react.bridge.Promise
6
6
  import com.facebook.react.bridge.ReactApplicationContext
7
7
  import com.facebook.react.bridge.ReactContextBaseJavaModule
8
8
  import com.facebook.react.bridge.ReactMethod
9
+ import com.facebook.react.bridge.ReadableMap
9
10
  import com.revenuecat.purchases.hybridcommon.ui.PaywallResultListener
10
11
  import com.revenuecat.purchases.hybridcommon.ui.PaywallSource
11
12
  import com.revenuecat.purchases.hybridcommon.ui.PresentPaywallOptions
@@ -38,6 +39,7 @@ internal class RNPaywallsModule(
38
39
  @ReactMethod
39
40
  fun presentPaywall(
40
41
  offeringIdentifier: String?,
42
+ presentedOfferingContext: ReadableMap?,
41
43
  displayCloseButton: Boolean?,
42
44
  fontFamily: String?,
43
45
  promise: Promise
@@ -45,6 +47,7 @@ internal class RNPaywallsModule(
45
47
  presentPaywall(
46
48
  null,
47
49
  offeringIdentifier,
50
+ presentedOfferingContext,
48
51
  displayCloseButton,
49
52
  fontFamily,
50
53
  promise
@@ -55,6 +58,7 @@ internal class RNPaywallsModule(
55
58
  fun presentPaywallIfNeeded(
56
59
  requiredEntitlementIdentifier: String,
57
60
  offeringIdentifier: String?,
61
+ presentedOfferingContext: ReadableMap?,
58
62
  displayCloseButton: Boolean,
59
63
  fontFamily: String?,
60
64
  promise: Promise
@@ -62,6 +66,7 @@ internal class RNPaywallsModule(
62
66
  presentPaywall(
63
67
  requiredEntitlementIdentifier,
64
68
  offeringIdentifier,
69
+ presentedOfferingContext,
65
70
  displayCloseButton,
66
71
  fontFamily,
67
72
  promise
@@ -81,6 +86,7 @@ internal class RNPaywallsModule(
81
86
  private fun presentPaywall(
82
87
  requiredEntitlementIdentifier: String?,
83
88
  offeringIdentifier: String?,
89
+ presentedOfferingContext: ReadableMap?,
84
90
  displayCloseButton: Boolean?,
85
91
  fontFamilyName: String?,
86
92
  promise: Promise
@@ -90,6 +96,11 @@ internal class RNPaywallsModule(
90
96
  FontAssetManager.getPaywallFontFamily(fontFamilyName = it, activity.resources.assets)
91
97
  }
92
98
 
99
+ val paywallSource: PaywallSource = offeringIdentifier?.let { offeringIdentifier ->
100
+ val presentedOfferingContextMap = RNPurchasesConverters.presentedOfferingContext(offeringIdentifier, presentedOfferingContext?.toHashMap())
101
+ PaywallSource.OfferingIdentifierWithPresentedOfferingContext(offeringIdentifier, presentedOfferingContext=presentedOfferingContextMap)
102
+ } ?: PaywallSource.DefaultOffering
103
+
93
104
  // @ReactMethod is not guaranteed to run on the main thread
94
105
  activity.runOnUiThread {
95
106
  presentPaywallFromFragment(
@@ -97,9 +108,7 @@ internal class RNPaywallsModule(
97
108
  PresentPaywallOptions(
98
109
  requiredEntitlementIdentifier = requiredEntitlementIdentifier,
99
110
  shouldDisplayDismissButton = displayCloseButton,
100
- paywallSource = offeringIdentifier?.let {
101
- PaywallSource.OfferingIdentifier(it)
102
- } ?: PaywallSource.DefaultOffering,
111
+ paywallSource = paywallSource,
103
112
  paywallResultListener = object : PaywallResultListener {
104
113
  override fun onPaywallResult(paywallResult: String) {
105
114
  promise.resolve(paywallResult)
@@ -1,9 +1,12 @@
1
1
  package com.revenuecat.purchases.react.ui
2
2
 
3
+ import com.facebook.react.bridge.ReadableMap
3
4
  import com.facebook.react.bridge.WritableArray
4
5
  import com.facebook.react.bridge.WritableMap
5
6
  import com.facebook.react.bridge.WritableNativeArray
6
7
  import com.facebook.react.bridge.WritableNativeMap
8
+ import com.revenuecat.purchases.PresentedOfferingContext
9
+ import kotlin.collections.get
7
10
 
8
11
  internal object RNPurchasesConverters {
9
12
 
@@ -45,4 +48,28 @@ internal object RNPurchasesConverters {
45
48
  }
46
49
  return writableArray
47
50
  }
51
+
52
+ fun presentedOfferingContext(offeringIdentifier: String, presentedOfferingContext: Map<*,*>?) : PresentedOfferingContext {
53
+ if (presentedOfferingContext == null) {
54
+ return PresentedOfferingContext(offeringIdentifier)
55
+ }
56
+
57
+ var targetingContext: PresentedOfferingContext.TargetingContext? = null;
58
+ val targetingContextMap = presentedOfferingContext["targetingContext"] as? Map<*, *>
59
+ if (targetingContextMap != null) {
60
+ val revision = (targetingContextMap["revision"] as? Number)?.toInt()
61
+ val ruleId = targetingContextMap["ruleId"] as? String
62
+ if (revision != null && ruleId != null) {
63
+ targetingContext = PresentedOfferingContext.TargetingContext(revision, ruleId)
64
+ }
65
+ }
66
+
67
+ val placementIdentifier = presentedOfferingContext["placementIdentifier"] as? String
68
+
69
+ return PresentedOfferingContext(
70
+ offeringIdentifier,
71
+ placementIdentifier,
72
+ targetingContext
73
+ )
74
+ }
48
75
  }
@@ -1,5 +1,6 @@
1
1
  package com.revenuecat.purchases.react.ui.customercenter.events
2
2
 
3
3
  internal enum class CustomerCenterEventName(val eventName: String) {
4
- ON_DISMISS("onDismiss");
4
+ ON_DISMISS("onDismiss"),
5
+ ON_CUSTOM_ACTION_SELECTED("onCustomActionSelected");
5
6
  }
@@ -2,6 +2,7 @@ package com.revenuecat.purchases.react.ui.views
2
2
 
3
3
  import android.content.Context
4
4
  import android.util.AttributeSet
5
+ import com.revenuecat.purchases.PresentedOfferingContext
5
6
  import com.revenuecat.purchases.ui.revenuecatui.PaywallListener
6
7
  import com.revenuecat.purchases.ui.revenuecatui.fonts.FontProvider
7
8
  import com.revenuecat.purchases.ui.revenuecatui.views.PaywallView
@@ -20,8 +21,8 @@ class WrappedPaywallComposeView(context: Context) : ComposeViewWrapper<PaywallVi
20
21
  wrappedView?.setDismissHandler(dismissHandler)
21
22
  }
22
23
 
23
- fun setOfferingId(offeringId: String?) {
24
- wrappedView?.setOfferingId(offeringId)
24
+ fun setOfferingId(offeringId: String?, presentedOfferingContext: PresentedOfferingContext? = null) {
25
+ wrappedView?.setOfferingId(offeringId, presentedOfferingContext)
25
26
  }
26
27
 
27
28
  fun setFontProvider(fontProvider: FontProvider?) {
@@ -2,6 +2,8 @@ package com.revenuecat.purchases.react.ui.views
2
2
 
3
3
  import android.content.Context
4
4
  import android.util.AttributeSet
5
+ import com.revenuecat.purchases.InternalRevenueCatAPI
6
+ import com.revenuecat.purchases.PresentedOfferingContext
5
7
  import com.revenuecat.purchases.ui.revenuecatui.PaywallListener
6
8
  import com.revenuecat.purchases.ui.revenuecatui.fonts.CustomFontProvider
7
9
  import com.revenuecat.purchases.ui.revenuecatui.views.OriginalTemplatePaywallFooterView
@@ -12,8 +14,16 @@ open class WrappedPaywallFooterComposeView(context: Context) : ComposeViewWrappe
12
14
  return OriginalTemplatePaywallFooterView(context, attrs)
13
15
  }
14
16
 
15
- fun setOfferingId(identifier: String) {
16
- wrappedView?.setOfferingId(identifier)
17
+ @OptIn(InternalRevenueCatAPI::class)
18
+ fun setOfferingId(offeringId: String?, presentedOfferingContext: PresentedOfferingContext? = null) {
19
+ if (offeringId == null) {
20
+ // We'll get rid of this deprecated API usage once https://github.com/RevenueCat/purchases-android/pull/2658 is merged
21
+ wrappedView?.setOfferingId(null)
22
+ }
23
+ else {
24
+ val presentedOfferingContext = presentedOfferingContext ?: PresentedOfferingContext(offeringId)
25
+ wrappedView?.setOfferingIdAndPresentedOfferingContext(offeringId, presentedOfferingContext)
26
+ }
17
27
  }
18
28
 
19
29
  fun setFontProvider(customFontProvider: CustomFontProvider) {
@@ -18,6 +18,8 @@ API_AVAILABLE(ios(15.0))
18
18
  @implementation CustomerCenterViewManager
19
19
 
20
20
  RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock)
21
+ RCT_EXPORT_VIEW_PROPERTY(onCustomActionSelected, RCTDirectEventBlock)
22
+ RCT_EXPORT_VIEW_PROPERTY(shouldShowCloseButton, BOOL)
21
23
  RCT_EXPORT_MODULE(CustomerCenterView)
22
24
 
23
25
  - (instancetype)init {
@@ -36,6 +38,10 @@ RCT_EXPORT_MODULE(CustomerCenterView)
36
38
 
37
39
  // Create a placeholder block that we'll update after wrapper creation
38
40
  __block CustomerCenterViewWrapper *wrapper = nil;
41
+
42
+ // Set onCloseHandler - always provide handler regardless of shouldShowCloseButton
43
+ // The close button visibility is controlled by shouldShowCloseButton property,
44
+ // but when the button IS shown, it needs this handler to function properly
39
45
  viewController.onCloseHandler = ^{
40
46
  if (wrapper && wrapper.onDismiss) {
41
47
  wrapper.onDismiss(nil);
@@ -13,6 +13,8 @@
13
13
  @interface CustomerCenterViewWrapper : UIView<RCCustomerCenterViewControllerDelegateWrapper>
14
14
 
15
15
  @property (nonatomic, copy) RCTDirectEventBlock onDismiss;
16
+ @property (nonatomic, copy) RCTDirectEventBlock onCustomActionSelected;
17
+ @property (nonatomic, assign) BOOL shouldShowCloseButton;
16
18
 
17
19
  - (instancetype)initWithCustomerCenterViewController:(CustomerCenterUIViewController *)viewController;
18
20
 
@@ -23,8 +23,9 @@ API_AVAILABLE(ios(15.0))
23
23
  - (instancetype)initWithCustomerCenterViewController:(CustomerCenterUIViewController *)viewController API_AVAILABLE(ios(15.0)) {
24
24
  NSParameterAssert(viewController);
25
25
 
26
- if ((self = [super initWithFrame:viewController.view.bounds])) {
26
+ if ((self = [super initWithFrame:CGRectZero])) { // Don't access the .view yet (rely on autolayout in layoutSubviews)
27
27
  _customerCenterVC = viewController;
28
+ _shouldShowCloseButton = YES; // Default to YES
28
29
  }
29
30
 
30
31
  return self;
@@ -40,6 +41,9 @@ API_AVAILABLE(ios(15.0))
40
41
  if (!self.addedToHierarchy) {
41
42
  UIViewController *parentController = self.parentViewController;
42
43
  if (parentController) {
44
+ // Configure the close button BEFORE accessing .view (which triggers viewDidLoad)
45
+ self.customerCenterVC.shouldShowCloseButton = self.shouldShowCloseButton;
46
+
43
47
  self.customerCenterVC.view.translatesAutoresizingMaskIntoConstraints = NO;
44
48
  [parentController addChildViewController:self.customerCenterVC];
45
49
  [self addSubview:self.customerCenterVC.view];
@@ -52,6 +56,7 @@ API_AVAILABLE(ios(15.0))
52
56
  [self.customerCenterVC.view.rightAnchor constraintEqualToAnchor:self.rightAnchor]
53
57
  ]];
54
58
 
59
+
55
60
  self.addedToHierarchy = YES;
56
61
  }
57
62
  }
@@ -63,4 +68,22 @@ API_AVAILABLE(ios(15.0))
63
68
  }
64
69
  }
65
70
 
71
+ - (void)customerCenterViewController:(CustomerCenterUIViewController *)controller
72
+ didSelectCustomAction:(NSString *)actionID
73
+ withPurchaseIdentifier:(NSString *)purchaseIdentifier API_AVAILABLE(ios(15.0)) {
74
+ if (self.onCustomActionSelected) {
75
+ self.onCustomActionSelected(@{@"actionId": actionID, @"purchaseIdentifier": purchaseIdentifier ?: [NSNull null]});
76
+ }
77
+ }
78
+
79
+ - (void)setShouldShowCloseButton:(BOOL)shouldShowCloseButton {
80
+ _shouldShowCloseButton = shouldShowCloseButton;
81
+
82
+ // Only set the view controller property if we haven't been added to hierarchy yet
83
+ // Once added to hierarchy, the property is already configured and shouldn't be changed
84
+ if (self.customerCenterVC && !self.addedToHierarchy) {
85
+ self.customerCenterVC.shouldShowCloseButton = shouldShowCloseButton;
86
+ }
87
+ }
88
+
66
89
  @end
@@ -10,6 +10,7 @@
10
10
  #import "UIView+React.h"
11
11
 
12
12
  @import PurchasesHybridCommonUI;
13
+ @import RevenueCat;
13
14
  @import RevenueCatUI;
14
15
 
15
16
  static NSString *const KeyCustomerInfo = @"customerInfo";
@@ -81,7 +82,9 @@ API_AVAILABLE(ios(15.0))
81
82
  if (offering && ![offering isKindOfClass:[NSNull class]]) {
82
83
  NSString *identifier = offering[@"identifier"];
83
84
  if (identifier) {
84
- [self.paywallViewController updateWithOfferingIdentifier:identifier];
85
+ RCPresentedOfferingContext *presentedOfferingContext = [self presentedOfferingContextFromOffering:offering];
86
+ [self.paywallViewController updateWithOfferingIdentifier:identifier
87
+ presentedOfferingContext:presentedOfferingContext];
85
88
  }
86
89
  }
87
90
 
@@ -100,6 +103,47 @@ API_AVAILABLE(ios(15.0))
100
103
  }
101
104
  }
102
105
 
106
+ - (RCPresentedOfferingContext *)presentedOfferingContextFromOffering:(NSDictionary *)offering {
107
+ NSArray *availablePackages = offering[@"availablePackages"];
108
+ if (!availablePackages || ![availablePackages isKindOfClass:[NSArray class]] || [availablePackages count] < 1) {
109
+ return nil;
110
+ }
111
+
112
+ NSDictionary *firstAvailablePackage = availablePackages[0];
113
+ if (!firstAvailablePackage || ![firstAvailablePackage isKindOfClass:[NSDictionary class]]) {
114
+ return nil;
115
+ }
116
+
117
+ NSDictionary *presentedOfferingContextOptions = firstAvailablePackage[@"presentedOfferingContext"];
118
+ if (!presentedOfferingContextOptions || [presentedOfferingContextOptions isKindOfClass:[NSNull class]]) {
119
+ return nil;
120
+ }
121
+
122
+ NSString *offeringIdentifier = presentedOfferingContextOptions[@"offeringIdentifier"];
123
+ if (!offeringIdentifier || [offeringIdentifier isKindOfClass:[NSNull class]]) {
124
+ return nil;
125
+ }
126
+
127
+ NSString *placementIdentifier = presentedOfferingContextOptions[@"placementIdentifier"];
128
+ if (![placementIdentifier isKindOfClass:[NSString class]]) {
129
+ placementIdentifier = nil;
130
+ }
131
+
132
+ RCTargetingContext *targetingContext;
133
+ NSDictionary *targetingContextOptions = presentedOfferingContextOptions[@"targetingContext"];
134
+ if (targetingContextOptions && ![targetingContextOptions isKindOfClass:[NSNull class]]) {
135
+ NSNumber *revision = targetingContextOptions[@"revision"];
136
+ NSString *ruleId = targetingContextOptions[@"ruleId"];
137
+ if (revision && [revision isKindOfClass:[NSNumber class]] && ruleId && [ruleId isKindOfClass:[NSString class]]) {
138
+ targetingContext = [[RCTargetingContext alloc] initWithRevision:[revision integerValue] ruleId:ruleId];
139
+ }
140
+ }
141
+
142
+ return [[RCPresentedOfferingContext alloc] initWithOfferingIdentifier:offeringIdentifier
143
+ placementIdentifier:placementIdentifier
144
+ targetingContext:targetingContext];
145
+ }
146
+
103
147
  - (void)paywallViewController:(RCPaywallViewController *)controller
104
148
  didStartPurchaseWithPackage:(NSDictionary *)packageDictionary API_AVAILABLE(ios(15.0)) {
105
149
  self.onPurchaseStarted(@{
@@ -59,6 +59,7 @@ RCT_EXPORT_MODULE();
59
59
  @"onRefundRequestCompleted",
60
60
  @"onFeedbackSurveyCompleted",
61
61
  @"onManagementOptionSelected",
62
+ @"onCustomActionSelected",
62
63
  @"onDismiss"
63
64
  ];
64
65
  }
@@ -142,6 +143,14 @@ withURL:(NSString *)url API_AVAILABLE(ios(15.0)) {
142
143
  [self sendEventWithName:@"onManagementOptionSelected" body:@{@"option": optionID, @"url": url ?: [NSNull null]}];
143
144
  }
144
145
 
146
+ - (void)customerCenterViewController:(CustomerCenterUIViewController *)controller
147
+ didSelectCustomAction:(NSString *)actionID
148
+ withPurchaseIdentifier:(NSString *)purchaseIdentifier {
149
+ [self sendEventWithName:@"onCustomActionSelected"
150
+ body:@{@"actionId": actionID, @"purchaseIdentifier": purchaseIdentifier ?: [NSNull null]}
151
+ ];
152
+ }
153
+
145
154
  + (BOOL)requiresMainQueueSetup
146
155
  {
147
156
  return YES;
package/ios/RNPaywalls.m CHANGED
@@ -62,6 +62,7 @@ RCT_EXPORT_MODULE();
62
62
  // MARK: -
63
63
 
64
64
  RCT_EXPORT_METHOD(presentPaywall:(nullable NSString *)offeringIdentifier
65
+ presentedOfferingContext:(nullable NSDictionary *)presentedOfferingContext
65
66
  shouldDisplayCloseButton:(BOOL)displayCloseButton
66
67
  withFontFamily:(nullable NSString *)fontFamily
67
68
  withResolve:(RCTPromiseResolveBlock)resolve
@@ -71,6 +72,9 @@ RCT_EXPORT_METHOD(presentPaywall:(nullable NSString *)offeringIdentifier
71
72
  if (offeringIdentifier != nil) {
72
73
  options[PaywallOptionsKeys.offeringIdentifier] = offeringIdentifier;
73
74
  }
75
+ if (presentedOfferingContext != nil) {
76
+ options[PaywallOptionsKeys.presentedOfferingContext] = presentedOfferingContext;
77
+ }
74
78
  options[PaywallOptionsKeys.displayCloseButton] = @(displayCloseButton);
75
79
  if (fontFamily) {
76
80
  options[PaywallOptionsKeys.fontName] = fontFamily;
@@ -87,6 +91,7 @@ RCT_EXPORT_METHOD(presentPaywall:(nullable NSString *)offeringIdentifier
87
91
 
88
92
  RCT_EXPORT_METHOD(presentPaywallIfNeeded:(NSString *)requiredEntitlementIdentifier
89
93
  withOfferingIdentifier:(nullable NSString *)offeringIdentifier
94
+ presentedOfferingContext:(nullable NSDictionary *)presentedOfferingContext
90
95
  shouldDisplayCloseButton:(BOOL)displayCloseButton
91
96
  withFontFamily:(nullable NSString *)fontFamily
92
97
  withResolve:(RCTPromiseResolveBlock)resolve
@@ -96,6 +101,9 @@ RCT_EXPORT_METHOD(presentPaywallIfNeeded:(NSString *)requiredEntitlementIdentifi
96
101
  if (offeringIdentifier != nil) {
97
102
  options[PaywallOptionsKeys.offeringIdentifier] = offeringIdentifier;
98
103
  }
104
+ if (presentedOfferingContext != nil) {
105
+ options[PaywallOptionsKeys.presentedOfferingContext] = presentedOfferingContext;
106
+ }
99
107
  options[PaywallOptionsKeys.requiredEntitlementIdentifier] = requiredEntitlementIdentifier;
100
108
  options[PaywallOptionsKeys.displayCloseButton] = @(displayCloseButton);
101
109
  if (fontFamily) {
@@ -127,9 +127,7 @@ const InternalPaywallFooterView = ({
127
127
 
128
128
  // Currently the same as the base type, but can be extended later if needed
129
129
 
130
- const InternalCustomerCenterView = _reactNative.UIManager.getViewManagerConfig('CustomerCenterView') != null ? (0, _reactNative.requireNativeComponent)('CustomerCenterView') : () => {
131
- throw new Error(LINKING_ERROR);
132
- };
130
+ const InternalCustomerCenterView = !usingPreviewAPIMode && _reactNative.UIManager.getViewManagerConfig('CustomerCenterView') != null ? (0, _reactNative.requireNativeComponent)('CustomerCenterView') : null;
133
131
 
134
132
  // This is to prevent breaking changes when the native SDK adds new options
135
133
 
@@ -160,8 +158,9 @@ class RevenueCatUI {
160
158
  displayCloseButton = RevenueCatUI.Defaults.PRESENT_PAYWALL_DISPLAY_CLOSE_BUTTON,
161
159
  fontFamily
162
160
  } = {}) {
161
+ var _offering$availablePa;
163
162
  RevenueCatUI.logWarningIfPreviewAPIMode("presentPaywall");
164
- return RNPaywalls.presentPaywall((offering === null || offering === void 0 ? void 0 : offering.identifier) ?? null, displayCloseButton, fontFamily);
163
+ return RNPaywalls.presentPaywall((offering === null || offering === void 0 ? void 0 : offering.identifier) ?? null, offering === null || offering === void 0 || (_offering$availablePa = offering.availablePackages) === null || _offering$availablePa === void 0 || (_offering$availablePa = _offering$availablePa[0]) === null || _offering$availablePa === void 0 ? void 0 : _offering$availablePa.presentedOfferingContext, displayCloseButton, fontFamily);
165
164
  }
166
165
 
167
166
  /**
@@ -183,8 +182,9 @@ class RevenueCatUI {
183
182
  displayCloseButton = RevenueCatUI.Defaults.PRESENT_PAYWALL_DISPLAY_CLOSE_BUTTON,
184
183
  fontFamily
185
184
  }) {
185
+ var _offering$availablePa2;
186
186
  RevenueCatUI.logWarningIfPreviewAPIMode("presentPaywallIfNeeded");
187
- return RNPaywalls.presentPaywallIfNeeded(requiredEntitlementIdentifier, (offering === null || offering === void 0 ? void 0 : offering.identifier) ?? null, displayCloseButton, fontFamily);
187
+ return RNPaywalls.presentPaywallIfNeeded(requiredEntitlementIdentifier, (offering === null || offering === void 0 ? void 0 : offering.identifier) ?? null, offering === null || offering === void 0 || (_offering$availablePa2 = offering.availablePackages) === null || _offering$availablePa2 === void 0 || (_offering$availablePa2 = _offering$availablePa2[0]) === null || _offering$availablePa2 === void 0 ? void 0 : _offering$availablePa2.presentedOfferingContext, displayCloseButton, fontFamily);
188
188
  }
189
189
  static Paywall = ({
190
190
  style,
@@ -310,13 +310,30 @@ class RevenueCatUI {
310
310
  */
311
311
  static CustomerCenterView = ({
312
312
  style,
313
- onDismiss
314
- }) => /*#__PURE__*/_react.default.createElement(InternalCustomerCenterView, {
315
- onDismiss: () => onDismiss && onDismiss(),
316
- style: [{
317
- flex: 1
318
- }, style]
319
- });
313
+ onDismiss,
314
+ onCustomActionSelected,
315
+ shouldShowCloseButton = true
316
+ }) => {
317
+ if (usingPreviewAPIMode) {
318
+ return /*#__PURE__*/_react.default.createElement(_previewComponents.PreviewCustomerCenter, {
319
+ onDismiss: () => onDismiss && onDismiss(),
320
+ style: [{
321
+ flex: 1
322
+ }, style]
323
+ });
324
+ }
325
+ if (!InternalCustomerCenterView) {
326
+ throw new Error(LINKING_ERROR);
327
+ }
328
+ return /*#__PURE__*/_react.default.createElement(InternalCustomerCenterView, {
329
+ onDismiss: () => onDismiss && onDismiss(),
330
+ onCustomActionSelected: event => onCustomActionSelected && onCustomActionSelected(event.nativeEvent),
331
+ shouldShowCloseButton: shouldShowCloseButton,
332
+ style: [{
333
+ flex: 1
334
+ }, style]
335
+ });
336
+ };
320
337
 
321
338
  /**
322
339
  * Presents the customer center to the user.
@@ -376,6 +393,12 @@ class RevenueCatUI {
376
393
  subscriptions.push(subscription);
377
394
  }
378
395
  }
396
+ if (callbacks.onCustomActionSelected) {
397
+ const subscription = customerCenterEventEmitter === null || customerCenterEventEmitter === void 0 ? void 0 : customerCenterEventEmitter.addListener('onCustomActionSelected', event => callbacks.onCustomActionSelected && callbacks.onCustomActionSelected(event));
398
+ if (subscription) {
399
+ subscriptions.push(subscription);
400
+ }
401
+ }
379
402
 
380
403
  // Return a promise that resolves when the customer center is dismissed
381
404
  return RNCustomerCenter.presentCustomerCenter().finally(() => {