From 1729b2b8b2c5628859782bae30e585011cab4460 Mon Sep 17 00:00:00 2001 From: NickSxti Date: Fri, 6 Mar 2026 17:32:08 +0400 Subject: [PATCH] Add @try/@catch exception guards to iOS TurboModule bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent NSExceptions from propagating to RCTTurboModule's performVoidMethodInvocation, which rethrows them as C++ exceptions causing std::terminate → SIGABRT on React Native New Architecture. - Void methods: catch and log via NSLog - Promise methods: catch and reject with QONBridgeException error code - Delegate callbacks: catch and log - dispatch_async blocks (NoCodes): guards placed inside the block Fixes: https://github.com/qonversion/react-native-sdk/issues/421 Upstream RN bug: facebook/react-native#54859 Co-Authored-By: Claude Opus 4.6 --- ios/RNNoCodes.mm | 93 ++++++++++++--- ios/RNQonversion.mm | 280 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 296 insertions(+), 77 deletions(-) diff --git a/ios/RNNoCodes.mm b/ios/RNNoCodes.mm index be15757..ea22024 100644 --- a/ios/RNNoCodes.mm +++ b/ios/RNNoCodes.mm @@ -5,6 +5,9 @@ #import "qonversion_react_native_sdk-Swift.h" #endif +#define QNR_LOG_EXCEPTION(method, exception) \ + NSLog(@"[Qonversion] Caught NSException in %s: %@ — %@", method, exception.name, exception.reason) + @interface RNNoCodes () @property (nonatomic, strong) RNNoCodesImpl *impl; @@ -22,79 +25,135 @@ - (instancetype)init { return self; } +#pragma mark - Void Methods + - (void)initialize:(NSString *)projectKey source:(NSString *)source version:(NSString *)version proxyUrl:(NSString *)proxyUrl locale:(NSString *)locale theme:(NSString *)theme { - [self.impl initializeWithProjectKey:projectKey source:source version:version proxyUrl:proxyUrl locale:locale theme:theme]; + @try { + [self.impl initializeWithProjectKey:projectKey source:source version:version proxyUrl:proxyUrl locale:locale theme:theme]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("initialize", exception); + } } - (void)setScreenPresentationConfig:(NSDictionary *)configData contextKey:(NSString *)contextKey resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl setScreenPresentationConfig:configData contextKey:contextKey]; + @try { + [self.impl setScreenPresentationConfig:configData contextKey:contextKey]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("setScreenPresentationConfig", exception); + } } - (void)showScreen:(NSString *)contextKey resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { dispatch_async(dispatch_get_main_queue(), ^{ - [self.impl showScreenWithContextKey:contextKey]; + @try { + [self.impl showScreenWithContextKey:contextKey]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("showScreen", exception); + } }); } - (void)close:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { dispatch_async(dispatch_get_main_queue(), ^{ - [self.impl close]; + @try { + [self.impl close]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("close", exception); + } }); } - (void)setPurchaseDelegate { - [self.impl setPurchaseDelegate:self]; + @try { + [self.impl setPurchaseDelegate:self]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("setPurchaseDelegate", exception); + } } - (void)delegatedPurchaseCompleted { - [self.impl delegatedPurchaseCompleted]; + @try { + [self.impl delegatedPurchaseCompleted]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("delegatedPurchaseCompleted", exception); + } } - (void)delegatedPurchaseFailed:(NSString *)errorMessage { - [self.impl delegatedPurchaseFailed:errorMessage]; + @try { + [self.impl delegatedPurchaseFailed:errorMessage]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("delegatedPurchaseFailed", exception); + } } - (void)delegatedRestoreCompleted { - [self.impl delegatedRestoreCompleted]; + @try { + [self.impl delegatedRestoreCompleted]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("delegatedRestoreCompleted", exception); + } } - (void)delegatedRestoreFailed:(NSString *)errorMessage { - [self.impl delegatedRestoreFailed:errorMessage]; + @try { + [self.impl delegatedRestoreFailed:errorMessage]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("delegatedRestoreFailed", exception); + } } - (void)setLocale:(NSString *)locale { - [self.impl setLocale:locale]; + @try { + [self.impl setLocale:locale]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("setLocale", exception); + } } - (void)setTheme:(NSString *)theme { - [self.impl setTheme:theme]; + @try { + [self.impl setTheme:theme]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("setTheme", exception); + } } -#pragma mark - NoCodesEventDelegate +#pragma mark - Delegate Callbacks - (void)noCodesDidTriggerWithEvent:(NSString * _Nonnull)event payload:(NSDictionary * _Nullable)payload { - [self emitOnNoCodeEvent:@{@"name": event, @"payload": payload ?: [NSNull null]}]; + @try { + [self emitOnNoCodeEvent:@{@"name": event, @"payload": payload ?: [NSNull null]}]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("noCodesDidTriggerWithEvent", exception); + } } -#pragma mark - NoCodesPurchaseDelegateProxy - - (void)purchase:(NSDictionary *)product { - [self emitOnNoCodePurchase:product]; + @try { + [self emitOnNoCodePurchase:product]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("purchase (NoCodes delegate)", exception); + } } - (void)restore { - [self emitOnNoCodeRestore]; + @try { + [self emitOnNoCodeRestore]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("restore (NoCodes delegate)", exception); + } } #pragma mark - TurboModule diff --git a/ios/RNQonversion.mm b/ios/RNQonversion.mm index 5a79412..2088da9 100644 --- a/ios/RNQonversion.mm +++ b/ios/RNQonversion.mm @@ -5,6 +5,9 @@ #import "qonversion_react_native_sdk-Swift.h" #endif +#define QNR_LOG_EXCEPTION(method, exception) \ + NSLog(@"[Qonversion] Caught NSException in %s: %@ — %@", method, exception.name, exception.reason) + @interface RNQonversion () @property (nonatomic, strong) RNQonversionImpl *impl; @@ -22,147 +25,304 @@ - (instancetype)init { return self; } +#pragma mark - Void Methods + - (void)storeSDKInfo:(nonnull NSString *)sdkName sdkVersion:(nonnull NSString *)sdkVersion { - [self.impl storeSDKInfo:sdkName version:sdkVersion]; + @try { + [self.impl storeSDKInfo:sdkName version:sdkVersion]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("storeSDKInfo", exception); + } } - (void)initializeSdk:(nonnull NSString *)projectKey launchMode:(nonnull NSString *)launchModeKey environment:(nonnull NSString *)environmentKey entitlementsCacheLifetime:(nonnull NSString *)cacheLifetimeKey proxyUrl:(NSString * _Nullable)proxyUrl kidsMode:(BOOL)kidsMode { - [self.impl initializeSdk:projectKey launchModeKey:launchModeKey environmentKey:environmentKey cacheLifetimeKey:cacheLifetimeKey proxyUrl:proxyUrl kidsMode:kidsMode]; + @try { + [self.impl initializeSdk:projectKey launchModeKey:launchModeKey environmentKey:environmentKey cacheLifetimeKey:cacheLifetimeKey proxyUrl:proxyUrl kidsMode:kidsMode]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("initializeSdk", exception); + } } - (void)syncHistoricalData { - [self.impl syncHistoricalData]; + @try { + [self.impl syncHistoricalData]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("syncHistoricalData", exception); + } } - (void)syncStoreKit2Purchases { - [self.impl syncStoreKit2Purchases]; + @try { + [self.impl syncStoreKit2Purchases]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("syncStoreKit2Purchases", exception); + } } -- (void)getPromotionalOffer:(nonnull NSString *)productId discount:(NSString * _Nullable)discountId resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - [self.impl getPromotionalOffer:productId discountId:discountId resolve:resolve reject:reject]; +- (void)setDefinedProperty:(NSString *)property value:(NSString *)value { + @try { + [self.impl setDefinedProperty:property value:value]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("setDefinedProperty", exception); + } } -- (void)purchase:(nonnull NSString *)productId quantity:(double)quantity contextKeys:(NSArray * _Nullable)contextKeys promoOffer:(JS::NativeQonversionModule::QPromoOfferDetails &)promoOffer offerId:(NSString * _Nullable)offerId applyOffer:(BOOL)applyOffer oldProductId:(NSString * _Nullable)oldProductId updatePolicyKey:(NSString * _Nullable)updatePolicyKey resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - NSDictionary *promoOfferDict = convertPromoOfferDetailsToDictionary(promoOffer); - [self.impl purchase:productId quantity:quantity contextKeys:contextKeys promoOffer:promoOfferDict resolve:resolve reject:reject]; +- (void)setCustomProperty:(NSString *)property value:(NSString *)value { + @try { + [self.impl setCustomProperty:property value:value]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("setCustomProperty", exception); + } } -- (void)purchaseWithResult:(nonnull NSString *)productId quantity:(double)quantity contextKeys:(NSArray * _Nullable)contextKeys promoOffer:(JS::NativeQonversionModule::QPromoOfferDetails &)promoOffer offerId:(NSString * _Nullable)offerId applyOffer:(BOOL)applyOffer oldProductId:(NSString * _Nullable)oldProductId updatePolicyKey:(NSString * _Nullable)updatePolicyKey resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - NSDictionary *promoOfferDict = convertPromoOfferDetailsToDictionary(promoOffer); - [self.impl purchaseWithResult:productId quantity:quantity contextKeys:contextKeys promoOffer:promoOfferDict resolve:resolve reject:reject]; +- (void)addAttributionData:(NSDictionary *)data provider:(NSString *)provider { + @try { + [self.impl addAttributionData:data provider:provider]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("addAttributionData", exception); + } } -- (void)setDefinedProperty:(NSString *)property value:(NSString *)value { - [self.impl setDefinedProperty:property value:value]; +- (void)logout { + @try { + [self.impl logout]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("logout", exception); + } } -- (void)setCustomProperty:(NSString *)property value:(NSString *)value { - [self.impl setCustomProperty:property value:value]; +- (void)collectAdvertisingId { + @try { + [self.impl collectAdvertisingId]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("collectAdvertisingId", exception); + } } -- (void)userProperties:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl userProperties:resolve reject:reject]; +- (void)collectAppleSearchAdsAttribution { + @try { + [self.impl collectAppleSearchAdsAttribution]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("collectAppleSearchAdsAttribution", exception); + } } -- (void)addAttributionData:(NSDictionary *)data provider:(NSString *)provider { - [self.impl addAttributionData:data provider:provider]; +- (void)presentCodeRedemptionSheet { + @try { + [self.impl presentCodeRedemptionSheet]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("presentCodeRedemptionSheet", exception); + } +} + +- (void)syncPurchases { + // Android only. +} + +#pragma mark - Promise Methods + +- (void)getPromotionalOffer:(nonnull NSString *)productId discount:(NSString * _Nullable)discountId resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { + @try { + [self.impl getPromotionalOffer:productId discountId:discountId resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("getPromotionalOffer", exception); + reject(@"QONBridgeException", exception.reason, nil); + } +} + +- (void)purchase:(nonnull NSString *)productId quantity:(double)quantity contextKeys:(NSArray * _Nullable)contextKeys promoOffer:(JS::NativeQonversionModule::QPromoOfferDetails &)promoOffer offerId:(NSString * _Nullable)offerId applyOffer:(BOOL)applyOffer oldProductId:(NSString * _Nullable)oldProductId updatePolicyKey:(NSString * _Nullable)updatePolicyKey resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { + @try { + NSDictionary *promoOfferDict = convertPromoOfferDetailsToDictionary(promoOffer); + [self.impl purchase:productId quantity:quantity contextKeys:contextKeys promoOffer:promoOfferDict resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("purchase", exception); + reject(@"QONBridgeException", exception.reason, nil); + } +} + +- (void)purchaseWithResult:(nonnull NSString *)productId quantity:(double)quantity contextKeys:(NSArray * _Nullable)contextKeys promoOffer:(JS::NativeQonversionModule::QPromoOfferDetails &)promoOffer offerId:(NSString * _Nullable)offerId applyOffer:(BOOL)applyOffer oldProductId:(NSString * _Nullable)oldProductId updatePolicyKey:(NSString * _Nullable)updatePolicyKey resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { + @try { + NSDictionary *promoOfferDict = convertPromoOfferDetailsToDictionary(promoOffer); + [self.impl purchaseWithResult:productId quantity:quantity contextKeys:contextKeys promoOffer:promoOfferDict resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("purchaseWithResult", exception); + reject(@"QONBridgeException", exception.reason, nil); + } +} + +- (void)userProperties:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + @try { + [self.impl userProperties:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("userProperties", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)checkEntitlements:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl checkEntitlements:resolve reject:reject]; + @try { + [self.impl checkEntitlements:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("checkEntitlements", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)products:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl products:resolve reject:reject]; + @try { + [self.impl products:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("products", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)offerings:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl offerings:resolve reject:reject]; + @try { + [self.impl offerings:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("offerings", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)checkTrialIntroEligibilityForProductIds:(NSArray *)data resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl checkTrialIntroEligibilityForProductIds:data resolve:resolve reject:reject]; + @try { + [self.impl checkTrialIntroEligibilityForProductIds:data resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("checkTrialIntroEligibility", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)restore:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl restore:resolve reject:reject]; + @try { + [self.impl restore:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("restore", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)identify:(NSString *)userId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl identify:userId resolve:resolve reject:reject]; -} - -- (void)logout { - [self.impl logout]; + @try { + [self.impl identify:userId resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("identify", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)userInfo:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl userInfo:resolve reject:reject]; + @try { + [self.impl userInfo:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("userInfo", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)remoteConfig:(NSString * _Nullable)contextKey resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl remoteConfig:contextKey resolve:resolve reject:reject]; + @try { + [self.impl remoteConfig:contextKey resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("remoteConfig", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)remoteConfigList:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl remoteConfigList:resolve reject:reject]; + @try { + [self.impl remoteConfigList:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("remoteConfigList", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)remoteConfigListForContextKeys:(NSArray *)contextKeys includeEmptyContextKey:(BOOL)includeEmptyContextKey resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl remoteConfigListForContextKeys:contextKeys includeEmptyContextKey:includeEmptyContextKey resolve:resolve reject:reject]; + @try { + [self.impl remoteConfigListForContextKeys:contextKeys includeEmptyContextKey:includeEmptyContextKey resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("remoteConfigListForContextKeys", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)attachUserToExperiment:(NSString *)experimentId groupId:(NSString *)groupId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl attachUserToExperiment:experimentId groupId:groupId resolve:resolve reject:reject]; + @try { + [self.impl attachUserToExperiment:experimentId groupId:groupId resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("attachUserToExperiment", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)detachUserFromExperiment:(NSString *)experimentId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl detachUserFromExperiment:experimentId resolve:resolve reject:reject]; + @try { + [self.impl detachUserFromExperiment:experimentId resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("detachUserFromExperiment", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)attachUserToRemoteConfiguration:(NSString *)remoteConfigurationId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl attachUserToRemoteConfiguration:remoteConfigurationId resolve:resolve reject:reject]; + @try { + [self.impl attachUserToRemoteConfiguration:remoteConfigurationId resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("attachUserToRemoteConfiguration", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)detachUserFromRemoteConfiguration:(NSString *)remoteConfigurationId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl detachUserFromRemoteConfiguration:remoteConfigurationId resolve:resolve reject:reject]; + @try { + [self.impl detachUserFromRemoteConfiguration:remoteConfigurationId resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("detachUserFromRemoteConfiguration", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)isFallbackFileAccessible:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl isFallbackFileAccessible:resolve reject:reject]; -} - -- (void)collectAdvertisingId { - [self.impl collectAdvertisingId]; -} - -- (void)collectAppleSearchAdsAttribution { - [self.impl collectAppleSearchAdsAttribution]; + @try { + [self.impl isFallbackFileAccessible:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("isFallbackFileAccessible", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)promoPurchase:(NSString *)storeProductId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { - [self.impl promoPurchase:storeProductId resolve:resolve reject:reject]; -} - -- (void)presentCodeRedemptionSheet { - [self.impl presentCodeRedemptionSheet]; -} - -- (void)syncPurchases { - // Android only. + @try { + [self.impl promoPurchase:storeProductId resolve:resolve reject:reject]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("promoPurchase", exception); + reject(@"QONBridgeException", exception.reason, nil); + } } - (void)updatePurchase:(nonnull NSString *)productId offerId:(NSString * _Nullable)offerId applyOffer:(BOOL)applyOffer oldProductId:(NSString * _Nullable)oldProductId updatePolicyKey:(NSString * _Nullable)updatePolicyKey resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { // Android only } +#pragma mark - Delegate Callbacks -- (void)qonversionDidReceiveUpdatedEntitlements:(NSDictionary * _Nonnull)entitlements { - [self emitOnEntitlementsUpdated:entitlements]; +- (void)qonversionDidReceiveUpdatedEntitlements:(NSDictionary * _Nonnull)entitlements { + @try { + [self emitOnEntitlementsUpdated:entitlements]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("qonversionDidReceiveUpdatedEntitlements", exception); + } } -- (void)shouldPurchasePromoProductWith:(NSString * _Nonnull)productId { - [self emitOnPromoPurchaseReceived:productId]; +- (void)shouldPurchasePromoProductWith:(NSString * _Nonnull)productId { + @try { + [self emitOnPromoPurchaseReceived:productId]; + } @catch (NSException *exception) { + QNR_LOG_EXCEPTION("shouldPurchasePromoProductWith", exception); + } } #pragma mark - Private