Contact Center AI Platform (CCAI Platform)은 React를 비롯한 여러 방법을 사용하여 모바일 애플리케이션과의 통합을 제공합니다. 이 문서에서는 iOS용 React Native를 통합하는 방법을 설명합니다.
이 Android 관련 안내를 따르기 전에 모바일 SDK용 React Native의 안내를 완료하세요.
CocoaPods를 사용하여 SDK 통합
CocoaPods를 사용하여 모바일 SD를 통합하려면 Podfile을 열고 다음 종속 항목을 타겟에 추가하세요.
UJETPodspec = { :podspec => 'https://sdk.ujet.co/ios/UJET.podspec' } pod 'UJET', UJETPodspec pod 'UJET/Cobrowse', UJETPodspecios 폴더에서
pod-install명령어를 실행하거나 app 폴더에서 npx pod-install을 실행하여 종속 항목을 설치합니다.
푸시 알림 설정
모바일 SDK에 푸시 알림을 설정하려면 다음 단계를 따르세요.
Xcode 프로젝트를 엽니다.
RCTAppDelegate를 준수하는 파일을 열고 다음 코드를 추가합니다.#import <UJETKit/UJETKit.h> #import <PushKit/PushKit.h> @interface AppDelegate() <PKPushRegistryDelegate> @end - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { PKPushRegistry *voipRegistry = [[PKPushRegistry alloc] initWithQueue: dispatch_get_main_queue()]; voipRegistry.delegate = self; voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; } #pragma mark PushKit - (NSString *)tokenFromData:(NSData *)data { const void *bytes = [data bytes]; const unsigned char *d = (const unsigned char *)bytes; NSMutableString *token = [NSMutableString string]; for (NSUInteger i = 0; i < [data length]; ++i) { [token appendFormat:@"%02X", d[i]]; } return [token lowercaseString]; } - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type { NSLog(@"voip token: %@", [self tokenFromData:credentials.token]); if ([type isEqual:PKPushTypeVoIP]) { [UJET updatePushToken:credentials.token type:UjetPushTypeVoIP]; } } - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type { if ([type isEqual:PKPushTypeVoIP]) { [UJET updatePushToken:nil type:UjetPushTypeVoIP]; } } - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion { if ([type isEqual:PKPushTypeVoIP] && payload.dictionaryPayload[@"ujet"]) { [UJET receivedNotification:payload.dictionaryPayload completion:completion]; } else { completion(); } } #pragma mark APNS - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { NSLog(@"apns token: %@", [self tokenFromData:deviceToken]); [UJET updatePushToken:deviceToken type:UjetPushTypeAPN]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(nonnull NSError *)error { [UJET updatePushToken:nil type:UjetPushTypeAPN]; } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { if (userInfo[@"ujet"]) { [UJET receivedNotification:userInfo completion:nil]; } else { // handle your notifications } }Swift의 경우:
import UJETKit import PushKit func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let voipRegistry = PKPushRegistry(queue: DispatchQueue.main) voipRegistry.desiredPushTypes = Set([PKPushType.voIP]) voipRegistry.delegate = self } // MARK: Push Notifications extension AppDelegate { func tokenFromData(data: Data) -> String { return data.map { String(format: "%02x", $0) }.joined() } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { print("apns token: ", tokenFromData(data: deviceToken)) UJET.updatePushToken(deviceToken, type: .APN) } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { UJET.updatePushToken(nil, type: .APN) } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if userInfo["ujet"] != nil { UJET.receivedNotification(userInfo, completion: nil) } else { // handle your notifications } } } // MARK: PushKit extension AppDelegate: PKPushRegistryDelegate { func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) { print("voip token: ", tokenFromData(data: credentials.token)) if type == .voIP { UJET.updatePushToken(credentials.token, type: .voIP) } } func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) { if type == .voIP { UJET.updatePushToken(nil, type: .voIP) } } func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { if type == .voIP && payload.dictionaryPayload["ujet"] != nil { UJET.receivedNotification(payload.dictionaryPayload, completion: completion) } else { completion() } } }.xcodeproj를 선택한 다음 앱 타겟을 선택합니다. 프레임워크, 라이브러리, 삽입된 콘텐츠 섹션에 PushKit.framework를 추가합니다.
딥 링크 설정
모바일 애플리케이션 내에서 딥 링크를 지원하려면 다음 코드를 사용하세요.
#pragma mark Deep Link
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
if ([url.scheme isEqualToString:@"ujet"]) {
return [self handleRouting:url];
}
return NO;
}
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void(^)(NSArray<id<UIUserActivityRestoring>> * __nullable restorableObjects))restorationHandler {
// Universal links
if ([NSUserActivityTypeBrowsingWeb isEqualToString:userActivity.activityType]) {
return [self handleRouting:userActivity.webpageURL];
} else if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]) {
// Open app from Call history
[UJET startWithOptions:nil];
return YES;
}
return NO;
}
- (BOOL)handleRouting:(NSURL *)url {
NSArray *availableSchema = @[
@"ujetrn", // TODO: Change to your custom URL scheme. Config from Portal > Developer Settings > Mobile App > Enable Send SMS to Download App > iOS App > URL
@"https" // TODO: or your universal link
];
NSArray *availableHostAndPath = @[
@"call", // custom URL scheme
@"ujet.cx/app" // universal link
];
if (![availableSchema containsObject:url.scheme]) {
return NO;
}
NSString *hostAndPath = [NSString stringWithFormat:@"%@%@", url.host, url.path];
if (![availableHostAndPath containsObject:hostAndPath]) {
return NO;
}
// ujetrn://call?call_id={call_id}&nonce={nonce}
// https://ujet.co/app?call_id={call_id}&nonce={nonce}
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url
resolvingAgainstBaseURL:NO];
NSArray *queryItems = urlComponents.queryItems;
NSString *callId = [self valueForKey:@"call_id" fromQueryItems:queryItems];
// validate call ID
if (![self isValidCallId:callId]) {
return NO;
}
NSString *nonce = [self valueForKey:@"nonce" fromQueryItems:queryItems];
UJETStartOptions *options = [[UJETStartOptions alloc] initWithCallId:callId nonce:nonce];
[UJET startWithOptions:options];
return YES;
}
- (NSString *)valueForKey:(NSString *)key fromQueryItems:(NSArray *)queryItems {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name=%@", key];
NSURLQueryItem *queryItem = [[queryItems
filteredArrayUsingPredicate:predicate]
firstObject];
return queryItem.value;
}
- (BOOL)isValidCallId:(NSString *)callId {
if (callId.length == 0) {
return NO;
}
NSCharacterSet *nonNumbers = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
NSRange r = [callId rangeOfCharacterFromSet: nonNumbers];
return r.location == NSNotFound;
}
Swift의 경우:
// MARK: Deep Link
extension AppDelegate {
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
print("Open app with url: \(url.absoluteString)")
return self.handleRouting(url)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
// Universal links
if NSUserActivityTypeBrowsingWeb == userActivity.activityType {
return self.handleRouting(userActivity.webpageURL!)
} else if userActivity.activityType == "INStartAudioCallIntent" {
// Open app from Call history
let model = DataController.shared.startOptions()
let startOptions = StartOptionsController().convert(model)
UJET.start(with: startOptions)
return true
}
return false
}
func handleRouting(_ url: URL) -> Bool {
let availableSchema = [
"ujetrn", // TODO: Change to your custom URL scheme. Config from Portal > Developer Settings > Mobile App > Enable Send SMS to Download App > iOS App > URL
"https" // universal link
]
let availableHostAndPath = [
"call", // custom URL scheme
"ujet.cx/app", // universal link,
]
if !(availableSchema.contains(url.scheme ?? "")) {
return false
}
let hostAndPath = String.init(format: "%@%@", url.host ?? "", url.path)
if !(availableHostAndPath.contains(hostAndPath)) {
return false
}
// ujet://call?call_id={call_id}&nonce={nonce}
// https://ujet.co/app?call_id={call_id}&nonce={nonce}
let urlComponents: URLComponents? = URLComponents(url: url, resolvingAgainstBaseURL: false)
let queryItems = urlComponents?.queryItems
let callId = value(forKey: "call_id", fromQueryItems: queryItems)
// validate call ID
if !isValidCallId(callId) {
return false
}
guard let nonce = value(forKey: "nonce", fromQueryItems: queryItems) else {
return false
}
let options = UJETStartOptions.init(callId: callId!, nonce: nonce)
UJET.start(with: options)
return true
}
func value(forKey key: String?, fromQueryItems queryItems: [URLQueryItem]?) -> String? {
let predicate = NSPredicate(format: "name=%@", key ?? "")
let filtered = (queryItems as NSArray?)?.filtered(using: predicate) as? [URLQueryItem]
let queryItem: URLQueryItem? = filtered?.first
return queryItem?.value
}
func isValidCallId(_ callId: String?) -> Bool {
if (callId ?? "").isEmpty {
return false
}
let nonNumbers = CharacterSet.decimalDigits.inverted
let r = callId?.rangeOfCharacter(from: nonNumbers)
return r == nil
}
}
타겟 구성
이 섹션에서는 타겟 구성에 필요한 기능과 추가 정보를 보여줍니다.
기능
푸시 알림
백그라운드 모드 (아래 항목 확인):
오디오 및 AirPlay
VoIP (Voice over IP)
정보
다음 키를 설명과 함께 추가합니다.
NSMicrophoneUsageDescription
NSCameraUsageDescription
NSPhotoLibraryUsageDescription
NSFaceIDUsageDescription