diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0d6f20b --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +.vscode/* +.gitignore +*.log +.prettierrc.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 997137e..5de39be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,9 @@ { "cSpell.words": [ + "autodetect", "barthelemy", + "bot", + "bot's", "chua", "cocoapod", "cocoapods", @@ -8,6 +11,13 @@ "kachanovskyi", "zanechua", "zendesk", + "zendesk's", "zopim" - ] + ], + "files.exclude": { + "**/.classpath": true, + "**/.project": true, + "**/.settings": true, + "**/.factorypath": true + } } diff --git a/README.md b/README.md index d5b9389..fc9550d 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ # react-native-zendesk-chat -Simple module that allows displaying Zopim Chat from Zendesk for React Native. +Simple module that supports displaying Zendesk Chat within a React Native Application. + +This library assumes you're familiar with Zendesk's Official Documentation: [iOS](https://developer.zendesk.com/embeddables/docs/chat-sdk-v-2-for-ios/introduction) and [Android](https://developer.zendesk.com/embeddables/docs/chat-sdk-v-2-for-android/introductionn). ## VERSIONS -- For RN version higher than 0.59 use version >= 0.3.0 (Zendesk Chat v1) -- For RN version lower than 0.59 use version <= 0.2.2 (Zendesk Chat v1) +- For **Zendesk Chat v2** use version >= 0.4.0 (this requires RN 0.59 or later!) +- For RN version >= 0.59 use version >= 0.3.0 (Zendesk Chat v1) +- For RN version < 0.59 use version <= 0.2.2 (Zendesk Chat v1) ## Known Issues ## Getting Started -Follow the instructions to install the SDK for [iOS](https://developer.zendesk.com/embeddables/docs/ios-chat-sdk/introduction) and [Android](https://developer.zendesk.com/embeddables/docs/android-chat-sdk/introduction). - With npm: `npm install react-native-zendesk-chat --save` @@ -33,7 +34,7 @@ $ (cd ios; pod install) # and see if there are any errors If you're on older react-native versions, please see the [Advanced Setup](#advanced-setup) section below -**Android** If you're on react-native >= 0.60, Android should autodetect this dependency. You may need to call `react-native link` +**Android** If you're on react-native >= 0.60, Android should autodetect this dependency. If you're on 0.59, you may need to call `react-native link` 2. Call the JS Initializer: @@ -54,9 +55,40 @@ ZendeskChat.startChat({ phone: user.mobile_phone, tags: ["tag1", "tag2"], department: "Your department", + // The behaviorFlags are optional, and each default to 'true' if omitted + behaviorFlags: { + showAgentAvailability: true, + showChatTranscriptPrompt: true, + showPreChatForm: true, + showOfflineForm: true, + }, + // The preChatFormOptions are optional & each defaults to "optional" if omitted + preChatFormOptions: { + name: !user.full_name ? "required" : "optional", + email: "optional", + phone: "optional", + department: "required", + }, + localizedDismissButtonTitle: "Dismiss", }); ``` +### Styling + +Changing the UI Styling is mostly achieved through native techniques. + +On Android, this is the [official documentation](https://developer.zendesk.com/embeddables/docs/android-unified-sdk/customize_the_look#how-theming-works) -- and an example might be adding these [3 lines to your app theme](https://github.com/zendesk/sdk_demo_app_android/blob/ae4c551f78911e983b0aac06967628f46be15e54/app/src/main/res/values/styles.xml#L5-L7) + +While on iOS, the options are more minimal -- check the [official doc page](https://developer.zendesk.com/embeddables/docs/chat-sdk-v-2-for-ios/customize_the_look#styling-the-chat-screen) + +### Migrating + +_From react-native-zendesk-chat <= 0.3.0_ + +To migrate from previous versions of the library, you should probably remove all integration steps you applied, and start over from the [Quick Start](#quickstart--usage). + +The JS API calls are very similar, with mostly additive changes. + ### Advanced Setup Advanced users, or users running on older versions of react-native may want to initialize things in native. @@ -110,16 +142,28 @@ project(':react-native-zendesk-chat').projectDir = new File(rootProject.projectD 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: +For RN >= 0.60: + +```gradle +dependencies { + // + api group: 'com.zendesk', name: 'chat', version: '2.2.0' + api group: 'com.zendesk', name: 'messaging', version: '4.3.1' +``` + +For RN < 0.60: + ```gradle compile project(':react-native-zendesk-chat') ``` -4. Configure `ZopimChat` in `android/app/main/java/[...]/MainActivity.java` +4. Configure `Chat` in `android/app/main/java/[...]/MainActivity.java` ```java -// Note: there is a JS method to do this! +// Note: there is a JS method to do this -- prefer doing that! -- This is for advanced users only. + // Call this once in your Activity's bootup lifecycle -ZopimChat.init("YOUR_ZENDESK_ACCOUNT_KEY").build(); +Chat.INSTANCE.init(mReactContext, key); ``` ## TODO diff --git a/RNZendeskChat.d.ts b/RNZendeskChat.d.ts index 2d759d3..c78d336 100644 --- a/RNZendeskChat.d.ts +++ b/RNZendeskChat.d.ts @@ -3,29 +3,83 @@ declare module "react-native-zendesk-chat" { name?: string; email?: string; phone?: string; + } - /** only implemented on iOS */ - shouldPersist?: boolean; + interface MessagingOptionsCommon { + /** Set this to set the bot's displayName */ + botName?: string; + } + interface MessagingOptions_iOS extends MessagingOptionsCommon { + /** Will be loaded using [UIImage imageWithName:…] ) */ + botAvatarName?: string; + /** Will be loaded using [UIImage imageWithData:[NSData dataWithContentsOfUrl:…]] */ + botAvatarUrl?: string; + } + interface MessagingOptions_Android extends MessagingOptionsCommon { + /** Will be loaded from your native asset resources */ + botAvatarDrawableId?: number; } - interface StartChatOptions_iOS extends VisitorInfoOptions { + + /** Current default is "optional" */ + type PreChatFormFieldOptionVisibility = "hidden" | "optional" | "required"; + + interface StartChatOptions extends VisitorInfoOptions { department?: string; tags?: string[]; - // Flags to disable some fields collected by default. - emailNotRequired?: boolean; - phoneNotRequired?: boolean; - departmentNotRequired?: boolean; - messageNotRequired?: boolean; + behaviorFlags?: { + /** if omitted, the form is enabled */ + showPreChatForm?: boolean; + /** if omitted, the prompt is enabled */ + showChatTranscriptPrompt?: boolean; + /** if omitted, the form is enabled */ + showOfflineForm?: boolean; + /** if omitted, the agent availability message is enabled */ + showAgentAvailability?: boolean; + }; + + /** If omitted, the preChatForm will be left as default in Zendesk */ + preChatFormOptions?: { + /** Should we ask the user for a contact email? */ + email?: PreChatFormFieldOptionVisibility; + /** Should we ask the user their name? */ + name?: PreChatFormFieldOptionVisibility; + /** Should we ask the user for their phone number? */ + phone?: PreChatFormFieldOptionVisibility; + /** Should we ask the user which department? */ + department?: PreChatFormFieldOptionVisibility; + }; + + /** + * Configure the Chat-Bot (if any) + */ + messagingOptions?: MessagingOptions_iOS & MessagingOptions_Android; + + /** + * If not provided, this will be "Close" -- not localized! + * + * -- iOS Only (Android: shows just a Back Button) + */ + localizedDismissButtonTitle?: string; } - type StartChatOptions_Android = VisitorInfoOptions; - type StartChatOptions = StartChatOptions_iOS & StartChatOptions_Android; class RNZendeskChatModuleImpl { + /** + * Must be called before calling startChat/setVisitorInfo + * - (Advanced users may configure this natively instead of calling this from JS) + */ init: (zendeskAccountKey: string) => void; - setVisitorInfo: (options: VisitorInfoOptions) => void; - + /** + * Presents the Zendesk Chat User Interface + */ startChat: (options: StartChatOptions) => void; + /** + * Backwards Compatibility! + * - You can pass all these parameters to RNZendeskChatModule.startChat + * - So you should probably prefer that method + */ + setVisitorInfo: (options: VisitorInfoOptions) => void; } const RNZendeskChatModule: RNZendeskChatModuleImpl; diff --git a/RNZendeskChat.podspec b/RNZendeskChat.podspec index b208b60..bdf2d3d 100644 --- a/RNZendeskChat.podspec +++ b/RNZendeskChat.podspec @@ -18,5 +18,5 @@ Pod::Spec.new do |s| s.framework = 'UIKit' s.dependency 'React' - s.dependency 'ZDCChat' + s.dependency 'ZendeskChatSDK' end \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 1b58419..ed86571 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -36,5 +36,7 @@ repositories { dependencies { implementation "com.facebook.react:react-native:+" - implementation group: 'com.zopim.android', name: 'sdk', version: '1.4.8' + + api group: 'com.zendesk', name: 'chat', version: safeExtGet('zendeskChatVersion', '2.2.0') + api group: 'com.zendesk', name: 'messaging', version: safeExtGet('zendeskMessagingVersion', '4.3.1') } diff --git a/android/src/main/java/com/taskrabbit/zendesk/RNZendeskChatModule.java b/android/src/main/java/com/taskrabbit/zendesk/RNZendeskChatModule.java index 6a2b6a4..7f43633 100644 --- a/android/src/main/java/com/taskrabbit/zendesk/RNZendeskChatModule.java +++ b/android/src/main/java/com/taskrabbit/zendesk/RNZendeskChatModule.java @@ -1,21 +1,133 @@ package com.taskrabbit.zendesk; import android.app.Activity; -import android.content.Intent; +import android.util.Log; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; -import com.zopim.android.sdk.api.ZopimChat; -import com.zopim.android.sdk.prechat.PreChatForm; -import com.zopim.android.sdk.model.VisitorInfo; -import com.zopim.android.sdk.prechat.ZopimChatActivity; +import com.facebook.react.bridge.ReadableType; +import com.facebook.react.bridge.WritableNativeMap; +import zendesk.chat.Chat; +import zendesk.chat.ChatConfiguration; +import zendesk.chat.ProfileProvider; +import zendesk.chat.PreChatFormFieldStatus; +import zendesk.chat.ChatEngine; +import zendesk.chat.VisitorInfo; +import zendesk.messaging.MessagingActivity; +import zendesk.messaging.MessagingConfiguration; import java.lang.String; +import java.util.ArrayList; public class RNZendeskChatModule extends ReactContextBaseJavaModule { + private static final String TAG = "[RNZendeskChatModule]"; + + private ArrayList currentUserTags = new ArrayList(); + + // private class Converters { + public static ArrayList getArrayListOfStrings(ReadableMap options, String key, String functionHint) { + ArrayList result = new ArrayList(); + + if (!options.hasKey(key)) { + return result; + } + if (options.getType(key) != ReadableType.Array) { + Log.e(RNZendeskChatModule.TAG, "wrong type for key '" + key + "' when processing " + functionHint + + ", expected an Array of Strings."); + return result; + } + ReadableArray arr = options.getArray(key); + for (int i = 0; i < arr.size(); i++) { + if (arr.isNull(i)) { + continue; + } + if (arr.getType(i) != ReadableType.String) { + Log.e(RNZendeskChatModule.TAG, "wrong type for key '" + key + "[" + i + "]' when processing " + + functionHint + ", expected entry to be a String."); + } + result.add(arr.getString(i)); + } + return result; + } + + public static String getStringOrNull(ReadableMap options, String key, String functionHint) { + if (!options.hasKey(key)) { + return null; + } + if (options.getType(key) != ReadableType.String) { + Log.e(RNZendeskChatModule.TAG, + "wrong type for key '" + key + "' when processing " + functionHint + ", expected a String."); + return null; + } + return options.getString(key); + } + + public static int getIntOrDefault(ReadableMap options, String key, String functionHint, int defaultValue) { + if (!options.hasKey(key)) { + return defaultValue; + } + if (options.getType(key) != ReadableType.String) { + Log.e(RNZendeskChatModule.TAG, + "wrong type for key '" + key + "' when processing " + functionHint + ", expected an Integer."); + return defaultValue; + } + return options.getInt(key); + } + + public static boolean getBooleanOrDefault(ReadableMap options, String key, String functionHint, + boolean defaultValue) { + if (!options.hasKey(key)) { + return defaultValue; + } + if (options.getType(key) != ReadableType.Boolean) { + Log.e(RNZendeskChatModule.TAG, + "wrong type for key '" + key + "' when processing " + functionHint + ", expected a Boolean."); + return defaultValue; + } + return options.getBoolean(key); + } + + public static PreChatFormFieldStatus getFieldStatusOrDefault(ReadableMap options, String key, + PreChatFormFieldStatus defaultValue) { + if (!options.hasKey(key)) { + return defaultValue; + } + if (options.getType(key) != ReadableType.String) { + Log.e(RNZendeskChatModule.TAG, "wrong type for key '" + key + + "' when processing startChat(preChatFormOptions), expected one of ('required' | 'optional' | 'hidden')."); + return defaultValue; + } + switch (options.getString(key)) { + case "required": + return PreChatFormFieldStatus.REQUIRED; + case "optional": + return PreChatFormFieldStatus.OPTIONAL; + case "hidden": + return PreChatFormFieldStatus.HIDDEN; + default: + Log.e(RNZendeskChatModule.TAG, "wrong type for key '" + key + + "' when processing startChat(preChatFormOptions), expected one of ('required' | 'optional' | 'hidden')."); + return defaultValue; + } + } + + public static ReadableMap getReadableMap(ReadableMap options, String key, String functionHint) { + if (!options.hasKey(key)) { + return new WritableNativeMap(); + } + if (options.getType(key) != ReadableType.Map) { + Log.e(RNZendeskChatModule.TAG, + "wrong type for key '" + key + "' when processing " + functionHint + ", expected a config hash."); + return new WritableNativeMap(); + } + return options.getMap(key); + } + // } + private ReactContext mReactContext; public RNZendeskChatModule(ReactApplicationContext reactContext) { @@ -30,43 +142,137 @@ public String getName() { @ReactMethod public void setVisitorInfo(ReadableMap options) { - VisitorInfo.Builder builder = new VisitorInfo.Builder(); + VisitorInfo.Builder builder = VisitorInfo.builder(); - if (options.hasKey("name")) { - builder.name(options.getString("name")); + String name = getStringOrNull(options, "name", "visitorInfo"); + if (name != null) { + builder = builder.withName(name); } - if (options.hasKey("email")) { - builder.email(options.getString("email")); + String email = getStringOrNull(options, "email", "visitorInfo"); + if (email != null) { + builder = builder.withEmail(name); } - if (options.hasKey("phone")) { - builder.phoneNumber(options.getString("phone")); + String phone = getStringOrNull(options, "phone", "visitorInfo"); + if (phone != null) { + builder = builder.withPhoneNumber(phone); } VisitorInfo visitorData = builder.build(); - ZopimChat.setVisitorInfo(visitorData); + if (Chat.INSTANCE.providers() == null) { + Log.e(TAG, + "Zendesk Internals are undefined -- did you forget to call RNZendeskModule.init()?"); + return; + } + Chat.INSTANCE.providers().profileProvider().setVisitorInfo(visitorData, null); } @ReactMethod public void init(String key) { - PreChatForm defaultPreChat = new PreChatForm.Builder() - .name(PreChatForm.Field.REQUIRED) - .email(PreChatForm.Field.REQUIRED) - .phoneNumber(PreChatForm.Field.REQUIRED) - .department(PreChatForm.Field.REQUIRED_EDITABLE) - .message(PreChatForm.Field.REQUIRED) - .build(); - ZopimChat.init(key) - .preChatForm(defaultPreChat) - .build(); + Chat.INSTANCE.init(mReactContext, key); + Log.d(TAG, "Chat.INSTANCE was properly initialized from JS."); + } + + private ChatConfiguration.Builder loadBehaviorFlags(ChatConfiguration.Builder b, ReadableMap options) { + boolean defaultValue = true; + String logHint = "startChat(behaviorFlags)"; + + return b.withPreChatFormEnabled(getBooleanOrDefault(options, "showPreChatForm", logHint, defaultValue)) + .withTranscriptEnabled(getBooleanOrDefault(options, "showChatTranscriptPrompt", logHint, defaultValue)) + .withOfflineFormEnabled(getBooleanOrDefault(options, "showOfflineForm", logHint, defaultValue)) + .withAgentAvailabilityEnabled( + getBooleanOrDefault(options, "showAgentAvailability", logHint, defaultValue)); + } + + private ChatConfiguration.Builder loadPreChatFormConfiguration(ChatConfiguration.Builder b, ReadableMap options) { + PreChatFormFieldStatus defaultValue = PreChatFormFieldStatus.OPTIONAL; + return b.withNameFieldStatus(getFieldStatusOrDefault(options, "name", defaultValue)) + .withEmailFieldStatus(getFieldStatusOrDefault(options, "email", defaultValue)) + .withPhoneFieldStatus(getFieldStatusOrDefault(options, "phone", defaultValue)) + .withDepartmentFieldStatus(getFieldStatusOrDefault(options, "department", defaultValue)); + } + + private void loadTags(ReadableMap options) { + // ZendeskChat Android treats the tags persistently, so you have to add/remove + // as things change -- aka doing a diff :-( + // ZendeskChat iOS just lets you override the full array so this isn't + // necessary on that side. + if (Chat.INSTANCE.providers() == null) { + Log.e(TAG, + "Zendesk Internals are undefined -- did you forget to call RNZendeskModule.init()?"); + return; + } + + ProfileProvider profileProvider = Chat.INSTANCE.providers().profileProvider(); + ArrayList activeTags = (ArrayList) currentUserTags.clone(); + + ArrayList allProvidedTags = RNZendeskChatModule.getArrayListOfStrings(options, "tags", "startChat"); + ArrayList newlyIntroducedTags = (ArrayList) allProvidedTags.clone(); + + newlyIntroducedTags.remove(activeTags); // Now just includes tags to add + currentUserTags.removeAll(allProvidedTags); // Now just includes tags to delete + + if (!currentUserTags.isEmpty()) { + profileProvider.removeVisitorTags(currentUserTags, null); + } + if (!newlyIntroducedTags.isEmpty()) { + profileProvider.addVisitorTags(newlyIntroducedTags, null); + } + + currentUserTags = allProvidedTags; + } + + private MessagingConfiguration.Builder loadBotSettings(ReadableMap options, + MessagingConfiguration.Builder builder) { + if (options == null) { + return builder; + } + String botName = getStringOrNull(options, "botName", "loadBotSettings"); + if (botName != null) { + builder = builder.withBotLabelString(botName); + } + int avatarDrawable = getIntOrDefault(options, "botAvatarDrawableId", "loadBotSettings", -1); + if (avatarDrawable != -1) { + builder = builder.withBotAvatarDrawable(avatarDrawable); + } + + return builder; } @ReactMethod public void startChat(ReadableMap options) { + if (Chat.INSTANCE.providers() == null) { + Log.e(TAG, + "Zendesk Internals are undefined -- did you forget to call RNZendeskModule.init()?"); + return; + } setVisitorInfo(options); + + ReadableMap flagHash = RNZendeskChatModule.getReadableMap(options, "behaviorFlags", "startChat"); + boolean showPreChatForm = getBooleanOrDefault(flagHash, "showPreChatForm", "startChat(behaviorFlags)", true); + + ChatConfiguration.Builder chatBuilder = loadBehaviorFlags(ChatConfiguration.builder(), flagHash); + if (showPreChatForm) { + chatBuilder = loadPreChatFormConfiguration(chatBuilder, + getReadableMap(options, "preChatFormOptions", "startChat")); + } + ChatConfiguration chatConfig = chatBuilder.build(); + + String department = RNZendeskChatModule.getStringOrNull(options, "department", "startChat"); + if (department != null) { + Chat.INSTANCE.providers().chatProvider().setDepartment(department, null); + } + + loadTags(options); + + MessagingConfiguration.Builder messagingBuilder = loadBotSettings( + getReadableMap(options, "messagingOptions", "startChat"), MessagingActivity.builder()); + Activity activity = getCurrentActivity(); if (activity != null) { - activity.startActivity(new Intent(mReactContext, ZopimChatActivity.class)); + messagingBuilder.withEngines(ChatEngine.engine()).show(activity, chatConfig); + } else { + Log.e(TAG, "Could not load getCurrentActivity -- no UI can be displayed without it."); } } } diff --git a/index.js b/index.js index 0d580a3..b4e9094 100644 --- a/index.js +++ b/index.js @@ -2,4 +2,9 @@ import { NativeModules } from "react-native"; const RNZendeskChatModule = NativeModules.RNZendeskChatModule; +/** + * TypeScript Documentation for this Module describes the available methods & parameters + * + * @see { ./RNZendeskChat.d.ts } + */ export default RNZendeskChatModule; diff --git a/ios/RNZendeskChatModule.m b/ios/RNZendeskChatModule.m index 4bde920..6384d9d 100644 --- a/ios/RNZendeskChatModule.m +++ b/ios/RNZendeskChatModule.m @@ -3,53 +3,192 @@ // Tasker // // Created by Jean-Richard Lai on 11/23/15. -// Copyright © 2015 Facebook. All rights reserved. // + #import "RNZendeskChatModule.h" -#import + +#import +#import + +#import +#import +#import +#import + + +@implementation RCTConvert (ZDKChatFormFieldStatus) + +RCT_ENUM_CONVERTER(ZDKFormFieldStatus, + (@{ + @"required": @(ZDKFormFieldStatusRequired), + @"optional": @(ZDKFormFieldStatusOptional), + @"hidden": @(ZDKFormFieldStatusHidden), + }), + ZDKFormFieldStatusOptional, + integerValue); + +@end + +@interface RNZendeskChatModule () +@end @implementation RNZendeskChatModule +// Backwards compatibility with the unnecessary setVisitorInfo method +ZDKChatAPIConfiguration *_visitorAPIConfig; + RCT_EXPORT_MODULE(RNZendeskChatModule); RCT_EXPORT_METHOD(setVisitorInfo:(NSDictionary *)options) { - [ZDCChat updateVisitor:^(ZDCVisitorInfo *visitor) { - if (options[@"name"]) { - visitor.name = options[@"name"]; - } - if (options[@"email"]) { - visitor.email = options[@"email"]; - } - if (options[@"phone"]) { - visitor.phone = options[@"phone"]; - } - visitor.shouldPersist = [options[@"shouldPersist"] boolValue] || NO; - }]; + if (!NSThread.isMainThread) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self setVisitorInfo:options]; + }); + return; + } + + ZDKChat.instance.configuration = _visitorAPIConfig = [self applyVisitorInfo:options intoConfig: _visitorAPIConfig ?: [[ZDKChatAPIConfiguration alloc] init]]; +} + +- (ZDKChatAPIConfiguration*)applyVisitorInfo:(NSDictionary*)options intoConfig:(ZDKChatAPIConfiguration*)config { + if (options[@"department"]) { + config.department = options[@"department"]; + } + if (options[@"tags"]) { + config.tags = options[@"tags"]; + } + config.visitorInfo = [[ZDKVisitorInfo alloc] initWithName:options[@"name"] + email:options[@"email"] + phoneNumber:options[@"phone"]]; + + NSLog(@"[RNZendeskChatModule] Applied visitor info: department: %@ tags: %@, email: %@, name: %@, phone: %@", config.department, config.tags, config.visitorInfo.email, config.visitorInfo.name, config.visitorInfo.phoneNumber); + return config; +} + +#define RNZDKConfigHashErrorLog(options, what)\ +if (!!options) {\ + NSLog(@"[RNZendeskChatModule] Invalid %@ -- expected a config hash", what);\ +} + +- (ZDKMessagingConfiguration *)messagingConfigurationFromConfig:(NSDictionary*)options { + ZDKMessagingConfiguration *config = [[ZDKMessagingConfiguration alloc] init]; + if (!options || ![options isKindOfClass:NSDictionary.class]) { + RNZDKConfigHashErrorLog(options, @"MessagingConfiguration config options"); + return config; + } + if (options[@"botName"]) { + config.name = options[@"botName"]; + } + + if (options[@"botAvatarName"]) { + config.botAvatar = [UIImage imageNamed:@"botAvatarName"]; + } else if (options[@"botAvatarUrl"]) { + config.botAvatar = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:options[@"botAvatarUrl"]]]]; + } + + return config; +} + +- (ZDKChatFormConfiguration * _Nullable)preChatFormConfigurationFromConfig:(NSDictionary*)options { + if (!options || ![options isKindOfClass:NSDictionary.class]) { + RNZDKConfigHashErrorLog(options, @"pre-Chat-Form Configuration Options"); + return nil; + } +#define ParseFormFieldStatus(key)\ + ZDKFormFieldStatus key = [RCTConvert ZDKFormFieldStatus:options[@"" #key]] + ParseFormFieldStatus(name); + ParseFormFieldStatus(email); + ParseFormFieldStatus(phone); + ParseFormFieldStatus(department); +#undef ParseFormFieldStatus + return [[ZDKChatFormConfiguration alloc] initWithName:name + email:email + phoneNumber:phone + department:department]; +} +- (ZDKChatConfiguration *)chatConfigurationFromConfig:(NSDictionary*)options { + options = options ?: @{}; + + ZDKChatConfiguration* config = [[ZDKChatConfiguration alloc] init]; + if (![options isKindOfClass:NSDictionary.class]){ + RNZDKConfigHashErrorLog(options, @"Chat Configuration Options"); + return config; + } + NSDictionary * behaviorFlags = options[@"behaviorFlags"]; + if (!behaviorFlags || ![behaviorFlags isKindOfClass:NSDictionary.class]) { + RNZDKConfigHashErrorLog(behaviorFlags, @"BehaviorFlags -- expected a config hash"); + behaviorFlags = NSDictionary.dictionary; + } + +#define ParseBehaviorFlag(key, target)\ +config.target = [RCTConvert BOOL: behaviorFlags[@"" #key] ?: @YES] + ParseBehaviorFlag(showPreChatForm, isPreChatFormEnabled); + ParseBehaviorFlag(showChatTranscriptPrompt, isChatTranscriptPromptEnabled); + ParseBehaviorFlag(showOfflineForm, isOfflineFormEnabled); + ParseBehaviorFlag(showAgentAvailability, isAgentAvailabilityEnabled); +#undef ParseBehaviorFlag + + if (config.isPreChatFormEnabled) { + ZDKChatFormConfiguration * formConfig = [self preChatFormConfigurationFromConfig:options[@"preChatFormOptions"]]; + if (!!formConfig) { + // Zendesk Swift Code crashes if you provide a nil form + config.preChatFormConfiguration = formConfig; + } + } + return config; } RCT_EXPORT_METHOD(startChat:(NSDictionary *)options) { - [self setVisitorInfo:options]; - - dispatch_sync(dispatch_get_main_queue(), ^{ - [ZDCChat startChat:^(ZDCConfig *config) { - if (options[@"department"]) { - config.department = options[@"department"]; - } - if (options[@"tags"]) { - config.tags = options[@"tags"]; - } - config.preChatDataRequirements.name = ZDCPreChatDataRequired; - config.preChatDataRequirements.email = options[@"emailNotRequired"] ? ZDCPreChatDataNotRequired : ZDCPreChatDataRequired; - config.preChatDataRequirements.phone = options[@"phoneNotRequired"] ? ZDCPreChatDataNotRequired : ZDCPreChatDataRequired; - config.preChatDataRequirements.department = options[@"departmentNotRequired"] ? ZDCPreChatDataNotRequired : ZDCPreChatDataRequiredEditable; - config.preChatDataRequirements.message = options[@"messageNotRequired"] ? ZDCPreChatDataNotRequired : ZDCPreChatDataRequired; - }]; - }); + if (!options || ![options isKindOfClass: NSDictionary.class]) { + if (!!options){ + NSLog(@"[RNZendeskChatModule] Invalid JS startChat Configuration Options -- expected a config hash"); + } + options = NSDictionary.dictionary; + } + + dispatch_sync(dispatch_get_main_queue(), ^{ + + ZDKChat.instance.configuration = [self applyVisitorInfo:options + intoConfig: _visitorAPIConfig ?: [[ZDKChatAPIConfiguration alloc] init]]; + + ZDKChatConfiguration * chatConfig = [self chatConfigurationFromConfig:options]; + + NSError *error = nil; + NSArray *engines = @[ + [ZDKChatEngine engineAndReturnError:&error] + ]; + if (!!error) { + NSLog(@"[RNZendeskChatModule] Internal Error loading ZDKChatEngine %@", error); + return; + } + + ZDKMessagingConfiguration *messagingConfig = [self messagingConfigurationFromConfig: options[@"messagingOptions"]]; + + UIViewController *viewController = [ZDKMessaging.instance buildUIWithEngines:engines + configs:@[chatConfig, messagingConfig] + error:&error]; + if (!!error) { + NSLog(@"[RNZendeskChatModule] Internal Error building ZDKMessagingUI %@",error); + return; + } + + viewController.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle: options[@"localizedDismissButtonTitle"] ?: @"Close" + style: UIBarButtonItemStylePlain + target: self + action: @selector(dismissChatUI)]; + + UINavigationController *chatController = [[UINavigationController alloc] initWithRootViewController: viewController]; + [RCTPresentedViewController() presentViewController:chatController animated:YES completion:nil]; + }); +} + +- (void) dismissChatUI { + [RCTPresentedViewController() dismissViewControllerAnimated:YES completion:nil]; } RCT_EXPORT_METHOD(init:(NSString *)zenDeskKey) { - [ZDCChat initializeWithAccountKey:zenDeskKey]; + [ZDKChat initializeWithAccountKey:zenDeskKey queue:dispatch_get_main_queue()]; } @end diff --git a/package.json b/package.json index 8dbbb29..98289db 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,6 @@ "devDependencies": { "prettier": "2.0.x", "eslint": "7.3.x" }, "peerDependencies": { "react": "^16.11.0", - "react-native": "^0.60.0" + "react-native": "0.60.x | 0.61.x | 0.62.x" } }