react-native-purchases-ui 9.10.5 → 9.11.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.
@@ -59,7 +59,7 @@ android {
59
59
  minSdkVersion getExtOrIntegerDefault("minSdkVersion")
60
60
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
61
61
  versionCode 1
62
- versionName '9.10.5'
62
+ versionName '9.11.0'
63
63
  }
64
64
 
65
65
  buildTypes {
@@ -12,8 +12,11 @@ import com.facebook.react.uimanager.annotations.ReactProp
12
12
  import com.facebook.react.uimanager.events.Event
13
13
  import com.revenuecat.purchases.Package
14
14
  import com.revenuecat.purchases.PresentedOfferingContext
15
+ import com.revenuecat.purchases.hybridcommon.ui.HybridPurchaseLogicBridge
15
16
  import com.revenuecat.purchases.hybridcommon.ui.PaywallListenerWrapper
16
17
  import com.revenuecat.purchases.react.ui.events.OnDismissEvent
18
+ import com.revenuecat.purchases.react.ui.events.OnPerformPurchaseEvent
19
+ import com.revenuecat.purchases.react.ui.events.OnPerformRestoreEvent
17
20
  import com.revenuecat.purchases.react.ui.events.OnPurchaseCancelledEvent
18
21
  import com.revenuecat.purchases.react.ui.events.OnPurchaseCompletedEvent
19
22
  import com.revenuecat.purchases.react.ui.events.OnPurchaseErrorEvent
@@ -24,6 +27,7 @@ import com.revenuecat.purchases.react.ui.events.OnRestoreErrorEvent
24
27
  import com.revenuecat.purchases.react.ui.events.OnRestoreStartedEvent
25
28
  import com.revenuecat.purchases.ui.revenuecatui.CustomVariableValue
26
29
  import com.revenuecat.purchases.ui.revenuecatui.fonts.CustomFontProvider
30
+ import java.util.concurrent.ConcurrentHashMap
27
31
 
28
32
  internal abstract class BasePaywallViewManager<T : View> : SimpleViewManager<T>() {
29
33
 
@@ -34,18 +38,23 @@ internal abstract class BasePaywallViewManager<T : View> : SimpleViewManager<T>(
34
38
  private const val OPTION_FONT_FAMILY = "fontFamily"
35
39
  private const val OPTION_DISPLAY_CLOSE_BUTTON = "displayCloseButton"
36
40
  private const val OPTION_CUSTOM_VARIABLES = "customVariables"
41
+ private const val OPTION_HAS_PURCHASE_LOGIC = "hasPurchaseLogic"
37
42
 
38
43
  private const val OPTION_OFFERING_AVAILABLE_PACKAGES = "availablePackages"
39
44
 
40
45
  private const val OPTION_OFFERING_AVAILABLE_PACKAGES_PRESENTED_OFFERING_CONTEXT = "presentedOfferingContext"
41
46
  }
42
47
 
48
+ private val purchaseLogicBridges = ConcurrentHashMap<Int, HybridPurchaseLogicBridge>()
49
+
43
50
  abstract fun setOfferingId(view: T, offeringId: String?, presentedOfferingContext: PresentedOfferingContext? = null)
44
51
 
45
52
  abstract fun setDisplayDismissButton(view: T, display: Boolean)
46
53
 
47
54
  abstract fun setCustomVariables(view: T, customVariables: Map<String, CustomVariableValue>)
48
55
 
56
+ abstract fun setPurchaseLogic(view: T, bridge: HybridPurchaseLogicBridge?)
57
+
49
58
  override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any>? {
50
59
  return MapBuilder.builder<String, Any>()
51
60
  .putEvent(PaywallEventName.ON_PURCHASE_STARTED)
@@ -58,6 +67,8 @@ internal abstract class BasePaywallViewManager<T : View> : SimpleViewManager<T>(
58
67
  .putEvent(PaywallEventName.ON_DISMISS)
59
68
  .putEvent(PaywallEventName.ON_MEASURE)
60
69
  .putEvent(PaywallEventName.ON_PURCHASE_PACKAGE_INITIATED)
70
+ .putEvent(PaywallEventName.ON_PERFORM_PURCHASE)
71
+ .putEvent(PaywallEventName.ON_PERFORM_RESTORE)
61
72
  .build()
62
73
  }
63
74
 
@@ -66,6 +77,7 @@ internal abstract class BasePaywallViewManager<T : View> : SimpleViewManager<T>(
66
77
  @ReactProp(name = PROP_OPTIONS)
67
78
  fun setOptions(view: T, options: ReadableMap?) {
68
79
  if (options != null) {
80
+ setPurchaseLogicProp(view, options)
69
81
  setOfferingIdProp(view, options)
70
82
  setFontFamilyProp(view, options)
71
83
  setDisplayCloseButton(view, options)
@@ -73,6 +85,11 @@ internal abstract class BasePaywallViewManager<T : View> : SimpleViewManager<T>(
73
85
  }
74
86
  }
75
87
 
88
+ override fun onDropViewInstance(view: T) {
89
+ purchaseLogicBridges.remove(view.id)?.cancelPending()
90
+ super.onDropViewInstance(view)
91
+ }
92
+
76
93
  private fun setOfferingIdProp(view: T, options: ReadableMap?) {
77
94
  val optionsMap = options?.toHashMap()
78
95
  if (optionsMap == null || !optionsMap.containsKey(OPTION_OFFERING)) {
@@ -135,6 +152,49 @@ internal abstract class BasePaywallViewManager<T : View> : SimpleViewManager<T>(
135
152
  }
136
153
  }
137
154
 
155
+ private fun setPurchaseLogicProp(view: T, options: ReadableMap) {
156
+ val hasPurchaseLogic = options.takeIf { it.hasKey(OPTION_HAS_PURCHASE_LOGIC) }
157
+ ?.getBoolean(OPTION_HAS_PURCHASE_LOGIC) ?: false
158
+ if (!hasPurchaseLogic) {
159
+ return
160
+ }
161
+
162
+ // Purchase logic is set once at view creation and cannot be changed later.
163
+ // Skip if already configured (e.g., on a React re-render).
164
+ if (purchaseLogicBridges.containsKey(view.id)) {
165
+ return
166
+ }
167
+
168
+ val themedReactContext = view.context as? ThemedReactContext ?: return
169
+
170
+ val bridge = HybridPurchaseLogicBridge(
171
+ onPerformPurchase = { eventData ->
172
+ val requestId = eventData[HybridPurchaseLogicBridge.EVENT_KEY_REQUEST_ID] as? String ?: return@HybridPurchaseLogicBridge
173
+ @Suppress("UNCHECKED_CAST")
174
+ val packageMap = eventData[HybridPurchaseLogicBridge.EVENT_KEY_PACKAGE_BEING_PURCHASED] as? Map<String, Any?> ?: emptyMap()
175
+ val event = OnPerformPurchaseEvent(
176
+ surfaceId = view.surfaceId,
177
+ viewTag = view.id,
178
+ packageMap = packageMap,
179
+ requestId = requestId,
180
+ )
181
+ emitEvent(themedReactContext, view.id, event)
182
+ },
183
+ onPerformRestore = { eventData ->
184
+ val requestId = eventData[HybridPurchaseLogicBridge.EVENT_KEY_REQUEST_ID] as? String ?: return@HybridPurchaseLogicBridge
185
+ val event = OnPerformRestoreEvent(
186
+ surfaceId = view.surfaceId,
187
+ viewTag = view.id,
188
+ requestId = requestId,
189
+ )
190
+ emitEvent(themedReactContext, view.id, event)
191
+ },
192
+ )
193
+
194
+ purchaseLogicBridges[view.id] = bridge
195
+ setPurchaseLogic(view, bridge)
196
+ }
197
+
138
198
  internal fun createPaywallListenerWrapper(
139
199
  themedReactContext: ThemedReactContext,
140
200
  view: View
@@ -10,7 +10,9 @@ internal enum class PaywallEventName(val eventName: String) {
10
10
  ON_RESTORE_ERROR("onRestoreError"),
11
11
  ON_DISMISS("onDismiss"),
12
12
  ON_MEASURE("onMeasure"),
13
- ON_PURCHASE_PACKAGE_INITIATED("onPurchasePackageInitiated");
13
+ ON_PURCHASE_PACKAGE_INITIATED("onPurchasePackageInitiated"),
14
+ ON_PERFORM_PURCHASE("onPerformPurchase"),
15
+ ON_PERFORM_RESTORE("onPerformRestore");
14
16
  }
15
17
 
16
18
  internal enum class PaywallEventKey(val key: String) {
@@ -3,6 +3,7 @@ package com.revenuecat.purchases.react.ui
3
3
  import androidx.core.view.children
4
4
  import com.facebook.react.uimanager.ThemedReactContext
5
5
  import com.revenuecat.purchases.PresentedOfferingContext
6
+ import com.revenuecat.purchases.hybridcommon.ui.HybridPurchaseLogicBridge
6
7
  import com.revenuecat.purchases.react.ui.events.OnMeasureEvent
7
8
  import com.revenuecat.purchases.react.ui.views.WrappedPaywallFooterComposeView
8
9
  import com.revenuecat.purchases.ui.revenuecatui.CustomVariableValue
@@ -94,4 +95,8 @@ internal class PaywallFooterViewManager : BasePaywallViewManager<WrappedPaywallF
94
95
  // No-op: Footer paywalls (legacy templates) don't support custom variables
95
96
  }
96
97
 
98
+ override fun setPurchaseLogic(view: WrappedPaywallFooterComposeView, bridge: HybridPurchaseLogicBridge?) {
99
+ // No-op: Footer paywalls don't support custom purchase logic
100
+ }
101
+
97
102
  }
@@ -2,6 +2,7 @@ package com.revenuecat.purchases.react.ui
2
2
 
3
3
  import com.facebook.react.uimanager.ThemedReactContext
4
4
  import com.revenuecat.purchases.PresentedOfferingContext
5
+ import com.revenuecat.purchases.hybridcommon.ui.HybridPurchaseLogicBridge
5
6
  import com.revenuecat.purchases.react.ui.views.WrappedPaywallComposeView
6
7
  import com.revenuecat.purchases.ui.revenuecatui.CustomVariableValue
7
8
  import com.revenuecat.purchases.ui.revenuecatui.fonts.CustomFontProvider
@@ -48,4 +49,8 @@ internal class PaywallViewManager : BasePaywallViewManager<WrappedPaywallCompose
48
49
  view.setCustomVariables(customVariables)
49
50
  }
50
51
 
52
+ override fun setPurchaseLogic(view: WrappedPaywallComposeView, bridge: HybridPurchaseLogicBridge?) {
53
+ view.setPurchaseLogic(bridge)
54
+ }
55
+
51
56
  }
@@ -7,6 +7,7 @@ import com.facebook.react.bridge.ReactApplicationContext
7
7
  import com.facebook.react.bridge.ReactContextBaseJavaModule
8
8
  import com.facebook.react.bridge.ReactMethod
9
9
  import com.facebook.react.bridge.ReadableMap
10
+ import com.revenuecat.purchases.hybridcommon.ui.HybridPurchaseLogicBridge
10
11
  import com.revenuecat.purchases.hybridcommon.ui.PaywallListenerWrapper
11
12
  import com.revenuecat.purchases.hybridcommon.ui.PaywallResultListener
12
13
  import com.revenuecat.purchases.hybridcommon.ui.PaywallSource
@@ -83,6 +84,11 @@ internal class RNPaywallsModule(
83
84
  PaywallListenerWrapper.resumePurchasePackageInitiated(requestId, shouldProceed)
84
85
  }
85
86
 
87
+ @ReactMethod
88
+ fun resolvePurchaseLogicResult(requestId: String, result: String, errorMessage: String?) {
89
+ HybridPurchaseLogicBridge.resolveResult(requestId, result, errorMessage)
90
+ }
91
+
86
92
  @ReactMethod
87
93
  fun addListener(eventName: String?) {
88
94
  // Keep: Required for RN built in Event Emitter Calls.
@@ -0,0 +1,24 @@
1
+ package com.revenuecat.purchases.react.ui.events
2
+
3
+ import com.facebook.react.bridge.WritableMap
4
+ import com.revenuecat.purchases.react.ui.PaywallEventKey
5
+ import com.revenuecat.purchases.react.ui.PaywallEventName
6
+
7
+ internal class OnPerformPurchaseEvent(
8
+ surfaceId: Int,
9
+ viewTag: Int,
10
+ private val packageMap: Map<String, Any?>,
11
+ private val requestId: String,
12
+ ) : PaywallEvent<OnPerformPurchaseEvent>(surfaceId, viewTag) {
13
+ override fun getPaywallEventName() = PaywallEventName.ON_PERFORM_PURCHASE
14
+
15
+ override fun getPayload() = mapOf(
16
+ PaywallEventKey.PACKAGE to packageMap,
17
+ )
18
+
19
+ override fun getEventData(): WritableMap {
20
+ return super.getEventData().apply {
21
+ putString(PaywallEventKey.REQUEST_ID.key, requestId)
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,21 @@
1
+ package com.revenuecat.purchases.react.ui.events
2
+
3
+ import com.facebook.react.bridge.WritableMap
4
+ import com.revenuecat.purchases.react.ui.PaywallEventKey
5
+ import com.revenuecat.purchases.react.ui.PaywallEventName
6
+
7
+ internal class OnPerformRestoreEvent(
8
+ surfaceId: Int,
9
+ viewTag: Int,
10
+ private val requestId: String,
11
+ ) : PaywallEvent<OnPerformRestoreEvent>(surfaceId, viewTag) {
12
+ override fun getPaywallEventName() = PaywallEventName.ON_PERFORM_RESTORE
13
+
14
+ override fun getPayload(): Map<PaywallEventKey, Map<String, Any?>> = emptyMap()
15
+
16
+ override fun getEventData(): WritableMap {
17
+ return super.getEventData().apply {
18
+ putString(PaywallEventKey.REQUEST_ID.key, requestId)
19
+ }
20
+ }
21
+ }
@@ -5,6 +5,7 @@ import android.util.AttributeSet
5
5
  import com.revenuecat.purchases.PresentedOfferingContext
6
6
  import com.revenuecat.purchases.ui.revenuecatui.CustomVariableValue
7
7
  import com.revenuecat.purchases.ui.revenuecatui.PaywallListener
8
+ import com.revenuecat.purchases.ui.revenuecatui.PurchaseLogic
8
9
  import com.revenuecat.purchases.ui.revenuecatui.fonts.FontProvider
9
10
  import com.revenuecat.purchases.ui.revenuecatui.views.PaywallView
10
11
 
@@ -38,6 +39,10 @@ class WrappedPaywallComposeView(context: Context) : ComposeViewWrapper<PaywallVi
38
39
  wrappedView?.setCustomVariables(customVariables)
39
40
  }
40
41
 
42
+ fun setPurchaseLogic(purchaseLogic: PurchaseLogic?) {
43
+ wrappedView?.setPurchaseLogic(purchaseLogic)
44
+ }
45
+
41
46
  override fun requestLayout() {
42
47
  super.requestLayout()
43
48
  post(measureAndLayout)
@@ -31,6 +31,8 @@ RCT_EXPORT_VIEW_PROPERTY(onRestoreCompleted, RCTDirectEventBlock)
31
31
  RCT_EXPORT_VIEW_PROPERTY(onRestoreError, RCTDirectEventBlock)
32
32
  RCT_EXPORT_VIEW_PROPERTY(onDismiss, RCTDirectEventBlock)
33
33
  RCT_EXPORT_VIEW_PROPERTY(onPurchasePackageInitiated, RCTDirectEventBlock)
34
+ RCT_EXPORT_VIEW_PROPERTY(onPerformPurchase, RCTDirectEventBlock)
35
+ RCT_EXPORT_VIEW_PROPERTY(onPerformRestore, RCTDirectEventBlock)
34
36
 
35
37
  RCT_EXPORT_MODULE(Paywall)
36
38
 
@@ -51,8 +53,15 @@ RCT_EXPORT_MODULE(Paywall)
51
53
  - (UIView *)view
52
54
  {
53
55
  if (@available(iOS 15.0, *)) {
54
- UIViewController *viewController = [self.proxy createPaywallView];
56
+ PaywallViewCreationParams *params = [PaywallViewCreationParams new];
57
+ RCPaywallViewController *viewController = [self.proxy createPaywallViewWithParams:params];
55
58
  PaywallViewWrapper *wrapper = [[PaywallViewWrapper alloc] initWithPaywallViewController:viewController];
59
+ PaywallProxy *proxy = self.proxy;
60
+ wrapper.createViewController = ^UIViewController * _Nullable(HybridPurchaseLogicBridge * _Nonnull bridge) {
61
+ PaywallViewCreationParams *params = [PaywallViewCreationParams new];
62
+ params.purchaseLogicBridge = bridge;
63
+ return [proxy createPaywallViewWithParams:params];
64
+ };
56
65
  self.proxy.delegate = wrapper;
57
66
 
58
67
  return wrapper;
@@ -22,6 +22,11 @@ NS_ASSUME_NONNULL_BEGIN
22
22
  @property (nonatomic, copy) RCTDirectEventBlock onRestoreError;
23
23
  @property (nonatomic, copy) RCTDirectEventBlock onDismiss;
24
24
  @property (nonatomic, copy, nullable) RCTDirectEventBlock onPurchasePackageInitiated;
25
+ @property (nonatomic, copy) RCTDirectEventBlock onPerformPurchase;
26
+ @property (nonatomic, copy) RCTDirectEventBlock onPerformRestore;
27
+
28
+ @property (nonatomic, strong, nullable) HybridPurchaseLogicBridge *purchaseLogicBridge;
29
+ @property (nonatomic, copy, nullable) UIViewController * _Nullable (^createViewController)(HybridPurchaseLogicBridge * _Nonnull);
25
30
 
26
31
  - (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
27
32
  - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
@@ -30,6 +30,10 @@ API_AVAILABLE(ios(15.0))
30
30
 
31
31
  @implementation PaywallViewWrapper
32
32
 
33
+ - (void)dealloc {
34
+ [self.purchaseLogicBridge cancelPending];
35
+ }
36
+
33
37
  - (instancetype)initWithPaywallViewController:(RCPaywallViewController *)paywallViewController API_AVAILABLE(ios(15.0)) {
34
38
  NSParameterAssert(paywallViewController);
35
39
 
@@ -89,6 +93,31 @@ API_AVAILABLE(ios(15.0))
89
93
 
90
94
  - (void)setOptions:(NSDictionary *)options {
91
95
  if (@available(iOS 15.0, *)) {
96
+ // Replace the default VC with one that uses custom purchase logic.
97
+ // This runs before the view is added to the hierarchy (RN sets props after view creation).
98
+ NSNumber *hasPurchaseLogicValue = options[@"hasPurchaseLogic"];
99
+ BOOL hasPurchaseLogic = [hasPurchaseLogicValue isKindOfClass:[NSNumber class]] ? [hasPurchaseLogicValue boolValue] : NO;
100
+ if (hasPurchaseLogic && self.addedToHierarchy) {
101
+ NSLog(@"RNPaywalls - Warning: Purchase logic must be set before the view is displayed. Ignoring.");
102
+ } else if (hasPurchaseLogic && self.createViewController != nil) {
103
+
104
+ __weak typeof(self) weakSelf = self;
105
+ HybridPurchaseLogicBridge *bridge = [[HybridPurchaseLogicBridge alloc]
106
+ initOnPerformPurchase:^(NSDictionary *eventData) {
107
+ if (weakSelf.onPerformPurchase) {
108
+ weakSelf.onPerformPurchase(eventData);
109
+ }
110
+ }
111
+ onPerformRestore:^(NSDictionary *eventData) {
112
+ if (weakSelf.onPerformRestore) {
113
+ weakSelf.onPerformRestore(eventData);
114
+ }
115
+ }];
116
+ self.purchaseLogicBridge = bridge;
117
+
118
+ self.paywallViewController = self.createViewController(bridge);
119
+ }
120
+
92
121
  self.pendingOptions = options ?: @{};
93
122
  self.didReceiveInitialOptions = YES;
94
123
 
@@ -152,27 +181,27 @@ API_AVAILABLE(ios(15.0))
152
181
  if (!availablePackages || ![availablePackages isKindOfClass:[NSArray class]] || [availablePackages count] < 1) {
153
182
  return nil;
154
183
  }
155
-
184
+
156
185
  NSDictionary *firstAvailablePackage = availablePackages[0];
157
186
  if (!firstAvailablePackage || ![firstAvailablePackage isKindOfClass:[NSDictionary class]]) {
158
187
  return nil;
159
188
  }
160
-
189
+
161
190
  NSDictionary *presentedOfferingContextOptions = firstAvailablePackage[@"presentedOfferingContext"];
162
191
  if (!presentedOfferingContextOptions || [presentedOfferingContextOptions isKindOfClass:[NSNull class]]) {
163
192
  return nil;
164
193
  }
165
-
194
+
166
195
  NSString *offeringIdentifier = presentedOfferingContextOptions[@"offeringIdentifier"];
167
196
  if (!offeringIdentifier || [offeringIdentifier isKindOfClass:[NSNull class]]) {
168
197
  return nil;
169
198
  }
170
-
199
+
171
200
  NSString *placementIdentifier = presentedOfferingContextOptions[@"placementIdentifier"];
172
201
  if (![placementIdentifier isKindOfClass:[NSString class]]) {
173
202
  placementIdentifier = nil;
174
203
  }
175
-
204
+
176
205
  RCTargetingContext *targetingContext;
177
206
  NSDictionary *targetingContextOptions = presentedOfferingContextOptions[@"targetingContext"];
178
207
  if (targetingContextOptions && ![targetingContextOptions isKindOfClass:[NSNull class]]) {
@@ -182,7 +211,7 @@ API_AVAILABLE(ios(15.0))
182
211
  targetingContext = [[RCTargetingContext alloc] initWithRevision:[revision integerValue] ruleId:ruleId];
183
212
  }
184
213
  }
185
-
214
+
186
215
  return [[RCPresentedOfferingContext alloc] initWithOfferingIdentifier:offeringIdentifier
187
216
  placementIdentifier:placementIdentifier
188
217
  targetingContext:targetingContext];
@@ -198,10 +227,12 @@ API_AVAILABLE(ios(15.0))
198
227
  - (void)paywallViewController:(RCPaywallViewController *)controller
199
228
  didFinishPurchasingWithCustomerInfoDictionary:(NSDictionary *)customerInfoDictionary
200
229
  transactionDictionary:(NSDictionary *)transactionDictionary API_AVAILABLE(ios(15.0)) {
201
- self.onPurchaseCompleted(@{
202
- KeyCustomerInfo: customerInfoDictionary,
203
- KeyStoreTransaction: transactionDictionary,
204
- });
230
+ NSMutableDictionary *event = [NSMutableDictionary dictionaryWithObject:customerInfoDictionary
231
+ forKey:KeyCustomerInfo];
232
+ if (transactionDictionary) {
233
+ event[KeyStoreTransaction] = transactionDictionary;
234
+ }
235
+ self.onPurchaseCompleted([event copy]);
205
236
  }
206
237
 
207
238
  - (void)paywallViewControllerDidCancelPurchase:(RCPaywallViewController *)controller API_AVAILABLE(ios(15.0)) {
package/ios/RNPaywalls.m CHANGED
@@ -134,6 +134,14 @@ RCT_EXPORT_METHOD(resumePurchasePackageInitiated:(NSString *)requestId
134
134
  }
135
135
  }
136
136
 
137
+ RCT_EXPORT_METHOD(resolvePurchaseLogicResult:(NSString *)requestId
138
+ result:(NSString *)result
139
+ errorMessage:(nullable NSString *)errorMessage) {
140
+ if (@available(iOS 15.0, *)) {
141
+ [HybridPurchaseLogicBridge resolveResultWithRequestId:requestId resultString:result errorMessage:errorMessage];
142
+ }
143
+ }
144
+
137
145
  - (void)rejectPaywallsUnsupportedError:(RCTPromiseRejectBlock)reject {
138
146
  NSLog(@"Error: attempted to present paywalls on unsupported iOS version.");
139
147
  reject(@"PaywallsUnsupportedCode", @"Paywalls are not supported prior to iOS 15.", nil);
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "PAYWALL_RESULT", {
15
15
  return _purchasesTypescriptInternal.PAYWALL_RESULT;
16
16
  }
17
17
  });
18
+ exports.PURCHASE_LOGIC_RESULT = void 0;
18
19
  Object.defineProperty(exports, "convertCustomVariablesToStringMap", {
19
20
  enumerable: true,
20
21
  get: function () {
@@ -38,7 +39,32 @@ var _customVariables = require("./customVariables");
38
39
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
39
40
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
40
41
  // Re-export for testing purposes (marked as @internal)
41
-
42
+ /**
43
+ * The result of a purchase or restore operation performed by custom app-based logic.
44
+ * Used when `purchasesAreCompletedBy` is set to `MY_APP`.
45
+ * @readonly
46
+ * @enum {string}
47
+ */
48
+ let PURCHASE_LOGIC_RESULT = exports.PURCHASE_LOGIC_RESULT = /*#__PURE__*/function (PURCHASE_LOGIC_RESULT) {
49
+ /** The purchase or restore was successful. */
50
+ PURCHASE_LOGIC_RESULT["SUCCESS"] = "SUCCESS";
51
+ /** The purchase was cancelled by the user. */
52
+ PURCHASE_LOGIC_RESULT["CANCELLATION"] = "CANCELLATION";
53
+ /** An error occurred during the purchase or restore. */
54
+ PURCHASE_LOGIC_RESULT["ERROR"] = "ERROR";
55
+ return PURCHASE_LOGIC_RESULT;
56
+ }({});
57
+ /**
58
+ * The result of a purchase or restore operation performed by custom purchase logic.
59
+ * Uses a discriminated union to allow structured error information.
60
+ */
61
+ /**
62
+ * Interface for handling purchases and restores within paywalls when
63
+ * `purchasesAreCompletedBy` is set to `MY_APP`.
64
+ *
65
+ * When provided, the paywall will call these functions instead of using
66
+ * RevenueCat's default purchase/restore behavior.
67
+ */
42
68
  const NATIVE_MODULE_NOT_FOUND_ERROR = `[RevenueCatUI] Native module not found. This can happen if:\n\n` + `- You are running in an unsupported environment (e.g., A browser or a container app that doesn't actually use the native modules)\n` + `- The native module failed to initialize\n` + `- react-native-purchases is not properly installed\n\n` + `To fix this:\n` + `- If using Expo, create a development build: https://docs.expo.dev/develop/development-builds/create-a-build/\n` + `- If using bare React Native, run 'pod install' and rebuild the app\n` + `- Make sure react-native-purchases is installed and you have rebuilt the app\n`;
43
69
 
44
70
  // Get the native module or use the preview implementation
@@ -52,16 +78,66 @@ function throwIfNativeModulesNotAvailable() {
52
78
  throw new Error(NATIVE_MODULE_NOT_FOUND_ERROR);
53
79
  }
54
80
  }
81
+
82
+ // Internal native props include purchase logic bridge events and native custom variable transforms
83
+
55
84
  const NativePaywall = !usingPreviewAPIMode && _reactNative.UIManager.getViewManagerConfig('Paywall') != null ? (0, _reactNative.requireNativeComponent)('Paywall') : null;
56
85
  const NativePaywallFooter = !usingPreviewAPIMode && _reactNative.UIManager.getViewManagerConfig('Paywall') != null ? (0, _reactNative.requireNativeComponent)('RCPaywallFooterView') : null;
57
86
 
58
87
  // Only create event emitters if native modules are available
59
88
  const eventEmitter = !usingPreviewAPIMode && RNPaywalls ? new _reactNative.NativeEventEmitter(RNPaywalls) : null;
60
89
  const customerCenterEventEmitter = !usingPreviewAPIMode && RNCustomerCenter ? new _reactNative.NativeEventEmitter(RNCustomerCenter) : null;
90
+ function resolveLogicResult(requestId, logicResult) {
91
+ const errorMessage = logicResult.result === PURCHASE_LOGIC_RESULT.ERROR && logicResult.error ? logicResult.error.message : null;
92
+ RNPaywalls === null || RNPaywalls === void 0 || RNPaywalls.resolvePurchaseLogicResult(requestId, logicResult.result, errorMessage);
93
+ }
94
+ function createPurchaseLogicHandlers(purchaseLogic) {
95
+ if (!purchaseLogic) {
96
+ return {
97
+ nativeOptions: {},
98
+ handlePerformPurchase: undefined,
99
+ handlePerformRestore: undefined
100
+ };
101
+ }
102
+ const nativeOptions = {
103
+ hasPurchaseLogic: true
104
+ };
105
+ const handlePerformPurchase = async event => {
106
+ const {
107
+ requestId,
108
+ packageBeingPurchased
109
+ } = event.nativeEvent;
110
+ try {
111
+ const logicResult = await purchaseLogic.performPurchase({
112
+ packageToPurchase: packageBeingPurchased
113
+ });
114
+ resolveLogicResult(requestId, logicResult);
115
+ } catch (e) {
116
+ RNPaywalls === null || RNPaywalls === void 0 || RNPaywalls.resolvePurchaseLogicResult(requestId, PURCHASE_LOGIC_RESULT.ERROR, e instanceof Error ? e.message : null);
117
+ }
118
+ };
119
+ const handlePerformRestore = async event => {
120
+ const {
121
+ requestId
122
+ } = event.nativeEvent;
123
+ try {
124
+ const logicResult = await purchaseLogic.performRestore();
125
+ resolveLogicResult(requestId, logicResult);
126
+ } catch (e) {
127
+ RNPaywalls === null || RNPaywalls === void 0 || RNPaywalls.resolvePurchaseLogicResult(requestId, PURCHASE_LOGIC_RESULT.ERROR, e instanceof Error ? e.message : null);
128
+ }
129
+ };
130
+ return {
131
+ nativeOptions,
132
+ handlePerformPurchase,
133
+ handlePerformRestore
134
+ };
135
+ }
61
136
  const InternalPaywall = ({
62
137
  style,
63
138
  children,
64
139
  options,
140
+ purchaseLogic,
65
141
  onPurchaseStarted,
66
142
  onPurchaseCompleted,
67
143
  onPurchaseError,
@@ -72,6 +148,11 @@ const InternalPaywall = ({
72
148
  onDismiss,
73
149
  onPurchasePackageInitiated
74
150
  }) => {
151
+ const {
152
+ nativeOptions,
153
+ handlePerformPurchase,
154
+ handlePerformRestore
155
+ } = createPurchaseLogicHandlers(purchaseLogic);
75
156
  if (usingPreviewAPIMode) {
76
157
  return /*#__PURE__*/_react.default.createElement(_previewComponents.PreviewPaywall, {
77
158
  offering: options === null || options === void 0 ? void 0 : options.offering,
@@ -88,11 +169,14 @@ const InternalPaywall = ({
88
169
  });
89
170
  } else if (!!NativePaywall) {
90
171
  // Transform options to native format (CustomVariables -> string map)
91
- const nativeOptions = (0, _customVariables.transformOptionsForNative)(options);
172
+ const transformedOptions = (0, _customVariables.transformOptionsForNative)(options);
92
173
  return /*#__PURE__*/_react.default.createElement(NativePaywall, {
93
174
  style: style,
94
175
  children: children,
95
- options: nativeOptions,
176
+ options: {
177
+ ...transformedOptions,
178
+ ...nativeOptions
179
+ },
96
180
  onPurchaseStarted: event => onPurchaseStarted && onPurchaseStarted(event.nativeEvent),
97
181
  onPurchaseCompleted: event => onPurchaseCompleted && onPurchaseCompleted(event.nativeEvent),
98
182
  onPurchaseError: event => onPurchaseError && onPurchaseError(event.nativeEvent),
@@ -117,7 +201,9 @@ const InternalPaywall = ({
117
201
  } else {
118
202
  RNPaywalls.resumePurchasePackageInitiated(requestId, true);
119
203
  }
120
- }
204
+ },
205
+ onPerformPurchase: handlePerformPurchase,
206
+ onPerformRestore: handlePerformRestore
121
207
  });
122
208
  }
123
209
  throw new Error(NATIVE_MODULE_NOT_FOUND_ERROR);
@@ -152,11 +238,11 @@ const InternalPaywallFooterView = ({
152
238
  });
153
239
  } else if (!!NativePaywallFooter) {
154
240
  // Transform options to native format (CustomVariables -> string map)
155
- const nativeOptions = (0, _customVariables.transformOptionsForNative)(options);
241
+ const transformedOptions = (0, _customVariables.transformOptionsForNative)(options);
156
242
  return /*#__PURE__*/_react.default.createElement(NativePaywallFooter, {
157
243
  style: style,
158
244
  children: children,
159
- options: nativeOptions,
245
+ options: transformedOptions,
160
246
  onPurchaseStarted: event => onPurchaseStarted && onPurchaseStarted(event.nativeEvent),
161
247
  onPurchaseCompleted: event => onPurchaseCompleted && onPurchaseCompleted(event.nativeEvent),
162
248
  onPurchaseError: event => onPurchaseError && onPurchaseError(event.nativeEvent),
@@ -178,16 +264,6 @@ const InternalPaywallFooterView = ({
178
264
  * @internal
179
265
  */
180
266
 
181
- /**
182
- * Native props for FullScreenPaywall component.
183
- * @internal
184
- */
185
-
186
- /**
187
- * Native props for FooterPaywall component.
188
- * @internal
189
- */
190
-
191
267
  const InternalCustomerCenterView = !usingPreviewAPIMode && _reactNative.UIManager.getViewManagerConfig('CustomerCenterView') != null ? (0, _reactNative.requireNativeComponent)('CustomerCenterView') : null;
192
268
 
193
269
  // This is to prevent breaking changes when the native SDK adds new options
@@ -255,6 +331,7 @@ class RevenueCatUI {
255
331
  style,
256
332
  children,
257
333
  options,
334
+ purchaseLogic,
258
335
  onPurchaseStarted,
259
336
  onPurchaseCompleted,
260
337
  onPurchaseError,
@@ -268,6 +345,7 @@ class RevenueCatUI {
268
345
  return /*#__PURE__*/_react.default.createElement(InternalPaywall, {
269
346
  options: options,
270
347
  children: children,
348
+ purchaseLogic: purchaseLogic,
271
349
  onPurchaseStarted: onPurchaseStarted,
272
350
  onPurchaseCompleted: onPurchaseCompleted,
273
351
  onPurchaseError: onPurchaseError,