适用于 iOS 的 React Native

Contact Center AI 平台 (CCAI Platform) 可通过多种方法(包括 React)与移动应用集成。本文档介绍了如何集成 React Native for iOS。

在按照这些特定于 Android 的说明操作之前,请先完成 React Native 移动 SDK 中的说明。

使用 CocoaPods 集成 SDK

  1. 如需使用 CocoaPods 集成移动 SDK,请打开 Podfile 并将以下依赖项添加到目标中:

    UJETPodspec = { :podspec => 'https://sdk.ujet.co/ios/UJET.podspec' }
    
    pod 'UJET', UJETPodspec
    pod 'UJET/Cobrowse', UJETPodspec
    
  2. 通过在 ios 文件夹中运行命令 pod-install 或在应用文件夹中运行 npx pod-install 来安装依赖项。

设置推送通知

如需为移动 SDK 设置推送通知,请按以下步骤操作:

  1. 打开 Xcode 项目。

  2. 打开符合 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()
            }
        }
    }
    
  3. 选择 .xcodeproj,然后选择您的应用目标。在“Frameworks, Libraries, and Embedded Content”(框架、库和嵌入内容)部分添加 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

    • IP 语音 (VoIP)

信息

添加以下键,并附上说明:

  • NSMicrophoneUsageDescription

  • NSCameraUsageDescription

  • NSPhotoLibraryUsageDescription

  • NSFaceIDUsageDescription