#import "RNBranch.h" #import #import #import #import "BranchLinkProperties+RNBranch.h" #import "BranchUniversalObject+RNBranch.h" #import "RNBranchAgingDictionary.h" #import "RNBranchEventEmitter.h" NSString * const RNBranchLinkOpenedNotification = @"RNBranchLinkOpenedNotification"; NSString * const RNBranchLinkOpenedNotificationErrorKey = @"error"; NSString * const RNBranchLinkOpenedNotificationParamsKey = @"params"; NSString * const RNBranchLinkOpenedNotificationUriKey = @"uri"; NSString * const RNBranchLinkOpenedNotificationBranchUniversalObjectKey = @"branch_universal_object"; NSString * const RNBranchLinkOpenedNotificationLinkPropertiesKey = @"link_properties"; static NSDictionary *initSessionWithLaunchOptionsResult; static NSURL *sourceUrl; static Branch *branchInstance; static NSString * const IdentFieldName = @"ident"; // These are only really exposed to the JS layer, so keep them internal for now. static NSString * const RNBranchErrorDomain = @"RNBranchErrorDomain"; static NSInteger const RNBranchUniversalObjectNotFoundError = 1; #pragma mark - Private RNBranch declarations @interface RNBranch() @property (nonatomic, readonly) UIViewController *currentViewController; @property (nonatomic) RNBranchAgingDictionary *universalObjectMap; @end #pragma mark - RNBranch implementation @implementation RNBranch @synthesize bridge = _bridge; RCT_EXPORT_MODULE(); - (NSDictionary *)constantsToExport { return @{ // RN events transmitted to JS by event emitter @"INIT_SESSION_SUCCESS": kRNBranchInitSessionSuccess, @"INIT_SESSION_ERROR": kRNBranchInitSessionError, // constants for use with userCompletedAction @"ADD_TO_CART_EVENT": BNCAddToCartEvent, @"ADD_TO_WISHLIST_EVENT": BNCAddToWishlistEvent, @"PURCHASED_EVENT": BNCPurchasedEvent, @"PURCHASE_INITIATED_EVENT": BNCPurchaseInitiatedEvent, @"REGISTER_VIEW_EVENT": BNCRegisterViewEvent, @"SHARE_COMPLETED_EVENT": BNCShareCompletedEvent, @"SHARE_INITIATED_EVENT": BNCShareInitiatedEvent }; } #pragma mark - Class methods + (void)useTestInstance { branchInstance = [Branch getTestInstance]; } //Called by AppDelegate.m -- stores initSession result in static variables and raises initSessionFinished event that's captured by the RNBranch instance to emit it to React Native + (void)initSessionWithLaunchOptions:(NSDictionary *)launchOptions isReferrable:(BOOL)isReferrable { sourceUrl = launchOptions[UIApplicationLaunchOptionsURLKey]; if (!branchInstance) { branchInstance = [Branch getInstance]; } [branchInstance initSessionWithLaunchOptions:launchOptions isReferrable:isReferrable andRegisterDeepLinkHandler:^(NSDictionary *params, NSError *error) { NSMutableDictionary *result = [NSMutableDictionary dictionary]; if (error) result[RNBranchLinkOpenedNotificationErrorKey] = error; if (params) { result[RNBranchLinkOpenedNotificationParamsKey] = params; if (params[@"~id"]) { BranchUniversalObject *branchUniversalObject = [BranchUniversalObject getBranchUniversalObjectFromDictionary:params]; if (branchUniversalObject) result[RNBranchLinkOpenedNotificationBranchUniversalObjectKey] = branchUniversalObject; BranchLinkProperties *linkProperties = [BranchLinkProperties getBranchLinkPropertiesFromDictionary:params]; if (linkProperties) result[RNBranchLinkOpenedNotificationLinkPropertiesKey] = linkProperties; } } if (sourceUrl) result[RNBranchLinkOpenedNotificationUriKey] = sourceUrl; [[NSNotificationCenter defaultCenter] postNotificationName:RNBranchLinkOpenedNotification object:nil userInfo:result]; }]; } + (BOOL)handleDeepLink:(NSURL *)url { sourceUrl = url; BOOL handled = [branchInstance handleDeepLink:url]; return handled; } + (BOOL)continueUserActivity:(NSUserActivity *)userActivity { sourceUrl = userActivity.webpageURL; return [branchInstance continueUserActivity:userActivity]; } #pragma mark - Object lifecycle - (instancetype)init { self = [super init]; if (self) { _universalObjectMap = [RNBranchAgingDictionary dictionaryWithTtl:3600.0]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onInitSessionFinished:) name:RNBranchLinkOpenedNotification object:nil]; } return self; } - (void) dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Utility methods - (UIViewController *)currentViewController { UIViewController *current = [UIApplication sharedApplication].keyWindow.rootViewController; while (current.presentedViewController && ![current.presentedViewController isKindOfClass:UIAlertController.class]) { current = current.presentedViewController; } return current; } - (void) onInitSessionFinished:(NSNotification*) notification { NSURL *uri = notification.userInfo[RNBranchLinkOpenedNotificationUriKey]; NSError *error = notification.userInfo[RNBranchLinkOpenedNotificationErrorKey]; NSDictionary *params = notification.userInfo[RNBranchLinkOpenedNotificationParamsKey]; initSessionWithLaunchOptionsResult = @{ RNBranchLinkOpenedNotificationErrorKey: error.localizedDescription ?: NSNull.null, RNBranchLinkOpenedNotificationParamsKey: params[@"~id"] ? params : NSNull.null, RNBranchLinkOpenedNotificationUriKey: uri.absoluteString ?: NSNull.null }; // If there is an error, fire error event if (error) { [RNBranchEventEmitter initSessionDidEncounterErrorWithPayload:initSessionWithLaunchOptionsResult]; } // otherwise notify the session is finished else { [RNBranchEventEmitter initSessionDidSucceedWithPayload:initSessionWithLaunchOptionsResult]; } } - (BranchLinkProperties*) createLinkProperties:(NSDictionary *)linkPropertiesMap withControlParams:(NSDictionary *)controlParamsMap { BranchLinkProperties *linkProperties = [[BranchLinkProperties alloc] initWithMap:linkPropertiesMap]; linkProperties.controlParams = controlParamsMap; return linkProperties; } - (BranchUniversalObject *)findUniversalObjectWithIdent:(NSString *)ident rejecter:(RCTPromiseRejectBlock)reject { BranchUniversalObject *universalObject = self.universalObjectMap[ident]; if (!universalObject) { NSString *errorMessage = [NSString stringWithFormat:@"BranchUniversalObject for ident %@ not found.", ident]; NSError *error = [NSError errorWithDomain:RNBranchErrorDomain code:RNBranchUniversalObjectNotFoundError userInfo:@{IdentFieldName : ident, NSLocalizedDescriptionKey: errorMessage }]; reject(@"RNBranch::Error::BUONotFound", errorMessage, error); } return universalObject; } #pragma mark - Methods exported to React Native #pragma mark redeemInitSessionResult RCT_EXPORT_METHOD( redeemInitSessionResult:(RCTPromiseResolveBlock)resolve rejecter:(__unused RCTPromiseRejectBlock)reject ) { resolve(initSessionWithLaunchOptionsResult ? initSessionWithLaunchOptionsResult : [NSNull null]); } #pragma mark setDebug RCT_EXPORT_METHOD( setDebug ) { [branchInstance setDebug]; } #pragma mark getLatestReferringParams RCT_EXPORT_METHOD( getLatestReferringParams:(RCTPromiseResolveBlock)resolve rejecter:(__unused RCTPromiseRejectBlock)reject ) { resolve([branchInstance getLatestReferringParams]); } #pragma mark getFirstReferringParams RCT_EXPORT_METHOD( getFirstReferringParams:(RCTPromiseResolveBlock)resolve rejecter:(__unused RCTPromiseRejectBlock)reject ) { resolve([branchInstance getFirstReferringParams]); } #pragma mark setIdentity RCT_EXPORT_METHOD( setIdentity:(NSString *)identity ) { [branchInstance setIdentity:identity]; } #pragma mark logout RCT_EXPORT_METHOD( logout ) { [branchInstance logout]; } #pragma mark userCompletedAction RCT_EXPORT_METHOD( userCompletedAction:(NSString *)event withState:(NSDictionary *)appState ) { [branchInstance userCompletedAction:event withState:appState]; } #pragma mark userCompletedActionOnUniversalObject RCT_EXPORT_METHOD( userCompletedActionOnUniversalObject:(NSString *)identifier event:(NSString *)event resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject ) { BranchUniversalObject *branchUniversalObject = [self findUniversalObjectWithIdent:identifier rejecter:reject]; if (!branchUniversalObject) return; [branchUniversalObject userCompletedAction:event]; resolve(NSNull.null); } #pragma mark userCompletedActionOnUniversalObject RCT_EXPORT_METHOD( userCompletedActionOnUniversalObject:(NSString *)identifier event:(NSString *)event state:(NSDictionary *)state resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject ) { BranchUniversalObject *branchUniversalObject = [self findUniversalObjectWithIdent:identifier rejecter:reject]; if (!branchUniversalObject) return; [branchUniversalObject userCompletedAction:event withState:state]; resolve(NSNull.null); } #pragma mark showShareSheet RCT_EXPORT_METHOD( showShareSheet:(NSString *)identifier withShareOptions:(NSDictionary *)shareOptionsMap withLinkProperties:(NSDictionary *)linkPropertiesMap withControlParams:(NSDictionary *)controlParamsMap resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject ) { BranchUniversalObject *branchUniversalObject = [self findUniversalObjectWithIdent:identifier rejecter:reject]; if (!branchUniversalObject) return; dispatch_async(dispatch_get_main_queue(), ^{ NSMutableDictionary *mutableControlParams = controlParamsMap.mutableCopy; if (shareOptionsMap && shareOptionsMap[@"emailSubject"]) { mutableControlParams[@"$email_subject"] = shareOptionsMap[@"emailSubject"]; } BranchLinkProperties *linkProperties = [self createLinkProperties:linkPropertiesMap withControlParams:mutableControlParams]; [branchUniversalObject showShareSheetWithLinkProperties:linkProperties andShareText:shareOptionsMap[@"messageBody"] fromViewController:self.currentViewController completionWithError:^(NSString * _Nullable activityType, BOOL completed, NSError * _Nullable activityError){ if (activityError) { NSString *errorCodeString = [NSString stringWithFormat:@"%ld", (long)activityError.code]; reject(errorCodeString, activityError.localizedDescription, activityError); return; } NSDictionary *result = @{ @"channel" : activityType ?: [NSNull null], @"completed" : @(completed), @"error" : [NSNull null] }; resolve(result); }]; }); } #pragma mark registerView RCT_EXPORT_METHOD( registerView:(NSString *)identifier resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject ) { BranchUniversalObject *branchUniversalObject = [self findUniversalObjectWithIdent:identifier rejecter:reject]; if (!branchUniversalObject) return; [branchUniversalObject registerViewWithCallback:^(NSDictionary *params, NSError *error) { if (!error) { resolve([NSNull null]); } else { reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); } }]; } #pragma mark generateShortUrl RCT_EXPORT_METHOD( generateShortUrl:(NSString *)identifier withLinkProperties:(NSDictionary *)linkPropertiesMap withControlParams:(NSDictionary *)controlParamsMap resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject ) { BranchUniversalObject *branchUniversalObject = [self findUniversalObjectWithIdent:identifier rejecter:reject]; if (!branchUniversalObject) return; BranchLinkProperties *linkProperties = [self createLinkProperties:linkPropertiesMap withControlParams:controlParamsMap]; [branchUniversalObject getShortUrlWithLinkProperties:linkProperties andCallback:^(NSString *url, NSError *error) { if (!error) { RCTLogInfo(@"RNBranch Success"); resolve(@{ @"url": url }); } else { reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); } }]; } #pragma mark listOnSpotlight RCT_EXPORT_METHOD( listOnSpotlight:(NSString *)identifier resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject ) { BranchUniversalObject *branchUniversalObject = [self findUniversalObjectWithIdent:identifier rejecter:reject]; if (!branchUniversalObject) return; [branchUniversalObject listOnSpotlightWithCallback:^(NSString *string, NSError *error) { if (!error) { NSDictionary *data = @{@"result":string}; resolve(data); } else { reject([NSString stringWithFormat: @"%lu", (long)error.code], error.localizedDescription, error); } }]; } // @TODO can this be removed? legacy, short url should be created from BranchUniversalObject #pragma mark getShortUrl RCT_EXPORT_METHOD( getShortUrl:(NSDictionary *)linkPropertiesMap resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject ) { NSString *feature = linkPropertiesMap[@"feature"]; NSString *channel = linkPropertiesMap[@"channel"]; NSString *stage = linkPropertiesMap[@"stage"]; NSArray *tags = linkPropertiesMap[@"tags"]; [branchInstance getShortURLWithParams:linkPropertiesMap andTags:tags andChannel:channel andFeature:feature andStage:stage andCallback:^(NSString *url, NSError *error) { if (error) { RCTLogError(@"RNBranch::Error: %@", error.localizedDescription); reject(@"RNBranch::Error", @"getShortURLWithParams", error); } resolve(url); }]; } #pragma mark loadRewards RCT_EXPORT_METHOD( loadRewards:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject ) { [branchInstance loadRewardsWithCallback:^(BOOL changed, NSError *error) { if(!error) { int credits = (int)[branchInstance getCredits]; resolve(@{@"credits": @(credits)}); } else { RCTLogError(@"Load Rewards Error: %@", error.localizedDescription); reject(@"RNBranch::Error::loadRewardsWithCallback", @"loadRewardsWithCallback", error); } }]; } #pragma mark redeemRewards RCT_EXPORT_METHOD( redeemRewards:(NSInteger)amount inBucket:(NSString *)bucket resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject ) { if (bucket) { [branchInstance redeemRewards:amount forBucket:bucket callback:^(BOOL changed, NSError *error) { if (!error) { resolve(@{@"changed": @(changed)}); } else { RCTLogError(@"Redeem Rewards Error: %@", error.localizedDescription); reject(@"RNBranch::Error::redeemRewards", error.localizedDescription, error); } }]; } else { [branchInstance redeemRewards:amount callback:^(BOOL changed, NSError *error) { if (!error) { resolve(@{@"changed": @(changed)}); } else { RCTLogError(@"Redeem Rewards Error: %@", error.localizedDescription); reject(@"RNBranch::Error::redeemRewards", error.localizedDescription, error); } }]; } } #pragma mark getCreditHistory RCT_EXPORT_METHOD( getCreditHistory:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject ) { [branchInstance getCreditHistoryWithCallback:^(NSArray *list, NSError *error) { if (!error) { resolve(list); } else { RCTLogError(@"Credit History Error: %@", error.localizedDescription); reject(@"RNBranch::Error::getCreditHistory", error.localizedDescription, error); } }]; } #pragma mark createUniversalObject RCT_EXPORT_METHOD( createUniversalObject:(NSDictionary *)universalObjectProperties resolver:(RCTPromiseResolveBlock)resolve rejecter:(__unused RCTPromiseRejectBlock)reject ) { BranchUniversalObject *universalObject = [[BranchUniversalObject alloc] initWithMap:universalObjectProperties]; NSString *identifier = [NSUUID UUID].UUIDString; self.universalObjectMap[identifier] = universalObject; NSDictionary *response = @{IdentFieldName: identifier}; resolve(response); } #pragma mark releaseUniversalObject RCT_EXPORT_METHOD( releaseUniversalObject:(NSString *)identifier ) { [self.universalObjectMap removeObjectForKey:identifier]; } @end