diff --git a/example/ios/IntercomReactNativeExample/AppDelegate.m b/example/ios/IntercomReactNativeExample/AppDelegate.m index 9d80aea8..c885c511 100644 --- a/example/ios/IntercomReactNativeExample/AppDelegate.m +++ b/example/ios/IntercomReactNativeExample/AppDelegate.m @@ -48,7 +48,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( #endif NSMutableDictionary *newLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; - + // Modifying launchOptions to facilitate deep linking. if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { NSDictionary *remoteNotif = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; @@ -59,7 +59,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( } } } - + RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:newLaunchOptions]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"IntercomReactNativeExample" @@ -79,7 +79,6 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( NSString *appId = [RNCConfig envFor:@"IOS_INTERCOM_APP_ID"]; [IntercomModule initialize:apiKey withAppId:appId]; - [self.window makeKeyAndVisible]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; diff --git a/ios/IntercomModule.m b/ios/IntercomModule.m index 5db7318c..10080a91 100644 --- a/ios/IntercomModule.m +++ b/ios/IntercomModule.m @@ -216,23 +216,68 @@ - (NSData *)dataFromHexString:(NSString *)string { RCT_EXPORT_METHOD(presentContent:(NSDictionary *)content resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { + // Validate content dictionary + if (!content || ![content isKindOfClass:[NSDictionary class]]) { + reject(@"INVALID_CONTENT", @"Content must be a valid dictionary", nil); + return; + } + IntercomContent *intercomContent; NSString *contentType = content[@"type"]; + + // Add nil check before calling methods + if (!contentType || ![contentType isKindOfClass:[NSString class]]) { + reject(@"INVALID_CONTENT_TYPE", @"Content type must be a valid string", nil); + return; + } + if ([contentType isEqualToString:@"ARTICLE"]) { - intercomContent = [IntercomContent articleWithId:content[@"id"]]; + NSString *articleId = content[@"id"]; + if (!articleId || ![articleId isKindOfClass:[NSString class]]) { + reject(@"INVALID_ARTICLE_ID", @"Article ID must be a valid string", nil); + return; + } + intercomContent = [IntercomContent articleWithId:articleId]; } else if ([contentType isEqualToString:@"CAROUSEL"]) { - intercomContent = [IntercomContent carouselWithId:content[@"id"]]; + NSString *carouselId = content[@"id"]; + if (!carouselId || ![carouselId isKindOfClass:[NSString class]]) { + reject(@"INVALID_CAROUSEL_ID", @"Carousel ID must be a valid string", nil); + return; + } + intercomContent = [IntercomContent carouselWithId:carouselId]; } else if ([contentType isEqualToString:@"SURVEY"]) { - intercomContent = [IntercomContent surveyWithId:content[@"id"]]; + NSString *surveyId = content[@"id"]; + if (!surveyId || ![surveyId isKindOfClass:[NSString class]]) { + reject(@"INVALID_SURVEY_ID", @"Survey ID must be a valid string", nil); + return; + } + intercomContent = [IntercomContent surveyWithId:surveyId]; } else if ([contentType isEqualToString:@"HELP_CENTER_COLLECTIONS"]) { - NSArray *collectionIds = content[@"ids"]; + NSArray *collectionIds = content[@"ids"]; + if (!collectionIds || ![collectionIds isKindOfClass:[NSArray class]]) { + reject(@"INVALID_COLLECTION_IDS", @"Collection IDs must be a valid array", nil); + return; + } intercomContent = [IntercomContent helpCenterCollectionsWithIds:collectionIds]; } else if ([contentType isEqualToString:@"CONVERSATION"]) { - intercomContent = [IntercomContent conversationWithId:content[@"id"]]; + NSString *conversationId = content[@"id"]; + if (!conversationId || ![conversationId isKindOfClass:[NSString class]]) { + reject(@"INVALID_CONVERSATION_ID", @"Conversation ID must be a valid string", nil); + return; + } + intercomContent = [IntercomContent conversationWithId:conversationId]; + } else { + reject(@"INVALID_CONTENT_TYPE", + [NSString stringWithFormat:@"Unknown content type: %@", contentType], + nil); + return; } + if (intercomContent) { [Intercom presentContent:intercomContent]; resolve(@(YES)); + } else { + reject(@"CONTENT_CREATION_FAILED", @"Failed to create Intercom content", nil); } }; diff --git a/src/index.tsx b/src/index.tsx index d30c6ed8..349ed6c1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,6 +7,45 @@ import { const { IntercomModule, IntercomEventEmitter } = NativeModules; +const createUnavailableError = (methodName: string) => + new Error( + `Intercom native module is unavailable. Cannot call ${methodName}. ` + + 'Make sure the native module is correctly linked and initialized.' + ); + +const rejectUnavailable = (methodName: string) => + Promise.reject(createUnavailableError(methodName)); + +const safeNativeCall = ( + methodName: string, + call?: () => Promise +): Promise => { + if (!call) { + return rejectUnavailable(methodName); + } + try { + return call(); + } catch (error) { + return Promise.reject(error); + } +}; + +const ensureString = (value: string, fallback = '') => + typeof value === 'string' ? value : fallback; + +const ensureNumber = (value: number, fallback = 0) => + typeof value === 'number' && Number.isFinite(value) ? value : fallback; + +const ensureArray = (value: T[] | undefined, fallback: T[] = []) => + Array.isArray(value) ? value : fallback; + +const ensureObject = function ( + value: T | undefined, + fallback: T +): T { + return value && typeof value === 'object' ? value : fallback; +}; + export enum Visibility { GONE = 'GONE', VISIBLE = 'VISIBLE', @@ -28,13 +67,20 @@ type LogLevelType = keyof typeof LogLevel; export const IntercomEvents = { IntercomUnreadCountDidChange: - IntercomEventEmitter.UNREAD_COUNT_CHANGE_NOTIFICATION, - IntercomWindowDidHide: IntercomEventEmitter.WINDOW_DID_HIDE_NOTIFICATION, - IntercomWindowDidShow: IntercomEventEmitter.WINDOW_DID_SHOW_NOTIFICATION, + IntercomEventEmitter?.UNREAD_COUNT_CHANGE_NOTIFICATION ?? + 'IntercomUnreadConversationCountDidChangeNotification', + IntercomWindowDidHide: + IntercomEventEmitter?.WINDOW_DID_HIDE_NOTIFICATION ?? + 'IntercomWindowDidHideNotification', + IntercomWindowDidShow: + IntercomEventEmitter?.WINDOW_DID_SHOW_NOTIFICATION ?? + 'IntercomWindowDidShowNotification', IntercomHelpCenterWindowDidShow: - IntercomEventEmitter.WINDOW_DID_SHOW_NOTIFICATION, + IntercomEventEmitter?.WINDOW_DID_SHOW_NOTIFICATION ?? + 'IntercomWindowDidShowNotification', IntercomHelpCenterWindowDidHide: - IntercomEventEmitter.WINDOW_DID_HIDE_NOTIFICATION, + IntercomEventEmitter?.WINDOW_DID_HIDE_NOTIFICATION ?? + 'IntercomWindowDidHideNotification', }; type EventType = @@ -298,62 +344,156 @@ export type IntercomType = { }; const Intercom: IntercomType = { - loginUnidentifiedUser: () => IntercomModule.loginUnidentifiedUser(), + loginUnidentifiedUser: () => + safeNativeCall( + 'loginUnidentifiedUser', + IntercomModule?.loginUnidentifiedUser + ), loginUserWithUserAttributes: (userAttributes) => - IntercomModule.loginUserWithUserAttributes(userAttributes), - logout: () => IntercomModule.logout(), - setUserHash: (hash) => IntercomModule.setUserHash(hash), - updateUser: (userAttributes) => IntercomModule.updateUser(userAttributes), - isUserLoggedIn: () => IntercomModule.isUserLoggedIn(), + safeNativeCall( + 'loginUserWithUserAttributes', + () => + IntercomModule?.loginUserWithUserAttributes?.( + ensureObject(userAttributes, {}) + ) + ), + logout: () => safeNativeCall('logout', IntercomModule?.logout), + setUserHash: (hash) => + safeNativeCall( + 'setUserHash', + () => IntercomModule?.setUserHash?.(ensureString(hash)) + ), + updateUser: (userAttributes) => + safeNativeCall( + 'updateUser', + () => IntercomModule?.updateUser?.(ensureObject(userAttributes, {})) + ), + isUserLoggedIn: () => + safeNativeCall('isUserLoggedIn', IntercomModule?.isUserLoggedIn), fetchLoggedInUserAttributes: () => - IntercomModule.fetchLoggedInUserAttributes(), + safeNativeCall( + 'fetchLoggedInUserAttributes', + IntercomModule?.fetchLoggedInUserAttributes + ), logEvent: (eventName, metaData = undefined) => - IntercomModule.logEvent(eventName, metaData), - - fetchHelpCenterCollections: () => IntercomModule.fetchHelpCenterCollections(), + safeNativeCall( + 'logEvent', + () => + IntercomModule?.logEvent?.( + ensureString(eventName), + metaData === undefined ? undefined : ensureObject(metaData, {}) + ) + ), + + fetchHelpCenterCollections: () => + safeNativeCall( + 'fetchHelpCenterCollections', + IntercomModule?.fetchHelpCenterCollections + ), fetchHelpCenterCollection: (id = '') => - IntercomModule.fetchHelpCenterCollection(id), - searchHelpCenter: (term = '') => IntercomModule.searchHelpCenter(term), - - present: () => IntercomModule.presentIntercom(), - presentSpace: (space) => IntercomModule.presentIntercomSpace(space), - presentContent: (content) => IntercomModule.presentContent(content), + safeNativeCall( + 'fetchHelpCenterCollection', + () => IntercomModule?.fetchHelpCenterCollection?.(ensureString(id)) + ), + searchHelpCenter: (term = '') => + safeNativeCall( + 'searchHelpCenter', + () => IntercomModule?.searchHelpCenter?.(ensureString(term)) + ), + + present: () => + safeNativeCall('presentIntercom', IntercomModule?.presentIntercom), + presentSpace: (space) => + safeNativeCall( + 'presentIntercomSpace', + () => IntercomModule?.presentIntercomSpace?.(space) + ), + presentContent: (content) => + safeNativeCall( + 'presentContent', + () => + IntercomModule?.presentContent?.( + ensureObject(content, { type: ContentType.Article }) + ) + ), presentMessageComposer: (initialMessage = undefined) => - IntercomModule.presentMessageComposer(initialMessage), - getUnreadConversationCount: () => IntercomModule.getUnreadConversationCount(), - - hideIntercom: () => IntercomModule.hideIntercom(), + safeNativeCall( + 'presentMessageComposer', + () => + IntercomModule?.presentMessageComposer?.( + initialMessage === undefined + ? undefined + : ensureString(initialMessage) + ) + ), + getUnreadConversationCount: () => + safeNativeCall( + 'getUnreadConversationCount', + IntercomModule?.getUnreadConversationCount + ), + + hideIntercom: () => + safeNativeCall('hideIntercom', IntercomModule?.hideIntercom), setBottomPadding: (paddingBottom) => - IntercomModule.setBottomPadding(paddingBottom), + safeNativeCall( + 'setBottomPadding', + () => IntercomModule?.setBottomPadding?.(ensureNumber(paddingBottom)) + ), setInAppMessageVisibility: (visibility) => - IntercomModule.setInAppMessageVisibility(visibility), + safeNativeCall( + 'setInAppMessageVisibility', + () => IntercomModule?.setInAppMessageVisibility?.(visibility) + ), setLauncherVisibility: (visibility) => - IntercomModule.setLauncherVisibility(visibility), + safeNativeCall( + 'setLauncherVisibility', + () => IntercomModule?.setLauncherVisibility?.(visibility) + ), setNeedsStatusBarAppearanceUpdate: Platform.select({ - ios: IntercomModule.setNeedsStatusBarAppearanceUpdate, + ios: () => + safeNativeCall( + 'setNeedsStatusBarAppearanceUpdate', + IntercomModule?.setNeedsStatusBarAppearanceUpdate + ), default: async () => true, }), handlePushMessage: Platform.select({ - android: IntercomModule.handlePushMessage, + android: () => + safeNativeCall('handlePushMessage', IntercomModule?.handlePushMessage), default: async () => true, }), - sendTokenToIntercom: (token) => IntercomModule.sendTokenToIntercom(token), - setLogLevel: (logLevel) => IntercomModule.setLogLevel(logLevel), + sendTokenToIntercom: (token) => + safeNativeCall( + 'sendTokenToIntercom', + () => IntercomModule?.sendTokenToIntercom?.(ensureString(token)) + ), + setLogLevel: (logLevel) => + safeNativeCall( + 'setLogLevel', + () => IntercomModule?.setLogLevel?.(logLevel) + ), addEventListener: (event, callback) => { + if (!IntercomEventEmitter) { + return { + remove: () => undefined, + } as EmitterSubscription; + } event === IntercomEvents.IntercomUnreadCountDidChange && Platform.OS === 'android' && - IntercomEventEmitter.startEventListener(); + IntercomEventEmitter.startEventListener?.(); const eventEmitter = new NativeEventEmitter(IntercomEventEmitter); - const listener = eventEmitter.addListener(event, callback); + const safeCallback = + typeof callback === 'function' ? callback : () => undefined; + const listener = eventEmitter.addListener(event, safeCallback); const originalRemove = listener.remove; listener.remove = () => { event === IntercomEvents.IntercomUnreadCountDidChange && Platform.OS === 'android' && - IntercomEventEmitter.removeEventListener(); + IntercomEventEmitter.removeEventListener?.(); originalRemove(); }; return listener; @@ -411,35 +551,35 @@ export const IntercomContent: IntercomContentType = { articleWithArticleId(articleId) { let articleContent = {} as Article; articleContent.type = ContentType.Article; - articleContent.id = articleId; + articleContent.id = ensureString(articleId); return articleContent; }, carouselWithCarouselId(carouselId) { let carouselContent = {} as Carousel; carouselContent.type = ContentType.Carousel; - carouselContent.id = carouselId; + carouselContent.id = ensureString(carouselId); return carouselContent; }, surveyWithSurveyId(surveyId) { let surveyContent = {} as Survey; surveyContent.type = ContentType.Survey; - surveyContent.id = surveyId; + surveyContent.id = ensureString(surveyId); return surveyContent; }, helpCenterCollectionsWithIds(collectionIds) { let helpCenterCollectionsContent = {} as HelpCenterCollections; helpCenterCollectionsContent.type = ContentType.HelpCenterCollections; - helpCenterCollectionsContent.ids = collectionIds; + helpCenterCollectionsContent.ids = ensureArray(collectionIds); return helpCenterCollectionsContent; }, conversationWithConversationId(conversationId) { let conversationContent = {} as Conversation; conversationContent.type = ContentType.Conversation; - conversationContent.id = conversationId; + conversationContent.id = ensureString(conversationId); return conversationContent; }, };