Documentation

React Native iOS

This details the steps that need to be taken once you have access to our iOS Keyboard SDK, in order to have a functional custom keyboard with onboarding sequence integrated into your React Native app.

Before Starting

Please contact Tappa to:

Get your CampaignID, without it, you will not be able to use the framework

System Requirements

To ensure seamless integration and optimal performance of the Tappa Keyboard SDK for iOS, the following system requirements must be met:

  • iOS Version: iOS 13.0 or newer.
  • React Native Version: Our SDK is compatible with React Native version 0.72.0 and above. This compatibility ensures access to the latest features and improvements in React Native, providing a robust foundation for incorporating the Tappa Keyboard SDK into your application.

0. Preliminary

Preliminary step for React Native:- choose the right node version

nvm install v16.13.1
  • Clean install the node modules
rm -rf node_modules
npm cache clean --force
npm install
  • CocoaPods:
  1. Podfile needs to be updated!

Check the example implementation for the code below and edit your own Podfile.

It needs to contain the pod for the Keyboard target.

It needs a post install step to add the SDK SPM to our two Mocha Pods libraries.

Both targets (main app and the keyboard) need to set use_frameworks!

Install the Pods

cd ios
pod install

1. Adding Keyboard Target

Create a new target in the Xcode project, a custom keyboard extension. Choose a meaningful name for it. Activate it when asked.
This will create a new folder in the project, with the chosen name, containing a template Info.plist file and a template KeyboardViewController.swift file.

Step 1.1: Change the bundle identifier for the extension to be <app_bundleID>.extension.

Step 1.2: Customize the Info.plist file setting RequestsOpenAccess to true.

Step 1.3: Customize the Deployment target for the keyboard extension to what you need for your project. But keep in mind that the SDK only works starting with iOS13. Also, all targets Deployment targets should match (or just set it once in the project)

Step 1.4: Install the custom keyboard on the device/simulator by activating from Settings/Keyboards or from Settings/< yourappname>. (for now)

Checkpoint: At this moment the iOS demo template keyboard should be active/selectable. It is empty for new devices (that have the next keyboard button in the bottom area). It only contains one button (next keyboard) for older devices.

2. Setting App Group Identifier

When integrating the SDK into your iOS application, it's crucial to correctly configure the Xcode project to ensure seamless operation between the main app and the custom keyboard extension. Follow these steps carefully:

  1. Setting Bundle Identifiers
    Your project will contain two targets: one for the main app and another for the custom keyboard extension. It's essential to configure their bundle identifiers correctly:

Main App Bundle Identifier: The bundle identifier for the main app should follow the standard format com.companyname.appname. Replace com.companyname and appname with your company's name and your app's name, respectively.

Keyboard Extension Bundle Identifier: The bundle identifier for the custom keyboard extension should be an extension of the main app's bundle identifier, formatted as com.companyname.appname.extension. This indicates that the keyboard is an extension of the main app.

  1. Configuring App Groups
    App Groups facilitate the sharing of data between the main app and the keyboard extension. To set this up:
  • Add the App Groups capability to both the main app and the keyboard extension targets in Xcode.
  • Create a new App Group with the identifier group.com.companyname.appname. Ensure this App Group is selected for both targets.
    This shared App Group identifier allows both the main application and the keyboard extension to access shared data securely.
  1. KBConfig.json Configuration
    The SDK relies on a configuration file named KBConfig.json to function correctly. Within this file, the package_name key must be set to the main app's bundle identifier:
{  
  "package_name": "com.companyname.appname"  
}

Note: For the app group entitlements we need to be granted access to the app’s record on the Dev Center because the app group has to be synced to the app id. If we do this via Xcode, it will automatically create and assign the App Group on the Developer Center. Verify, just in case it didn't, and if so, create it manually.

3. Add the keyboard SDK

Step 3.0: Add the following SPM package to your app: Tappa Keyboard SDK([email protected]/tappa-keyboards/package-keemoji-ios-framework) and link as Frameworks and Libraries to both the app target and the keyboard target.

Step 3.1: Create your own Resources module. For a guide on how to obtain a Resources module, please read the Customisation section.

Customisation

Add the files as source files (or you can package them if you prefer) to both the app target and the keyboard target.

Step 3.2 Setup the Keyboard: 

  • Delete the auto created template KeyboardViewController.swift file, it clashes with the one from Mocha
  • Modify info.plist NSExtensionPrincipalClass in Keyboard target like this:
<key>NSExtensionPrincipalClass</key>
<string>mochaglobal_extension_keyboard_sdk.KeyboardViewController</string>
  • Create empty file Dummy.swift in Keyboard target
  • In the standard installation, you don’t need swift files in the app, and you don’t need ObjC files in the Keyboard. So there’s no need for Bridging headers, in case you get asked the question. (Only if you are adding some other files of your own.)
  • Optional, but strongly recommended: To have a unified display of the keyboard name in the selection list of keyboards, it is recommended that the customer sets the value of CFBundleDisplayName for the keyboard to the same name as the CFBundleDisplayName of the app. This can be done in the Info.plist file or in the Build Settings. In that case the operating system does not use the <keyboard_name> - <app_name> convention, but the <display_name> convention. Ex: “Tappa”

Step 3.3: KBCustomization.swift

In KBCustomization folder also create KBCustomization.swift with the following content:

import UIKit
import Keemoji

/// KBCustomization implementation of the KMCustomizable protocol
/// Any client integrating the SDK, would have to provide a similar implementation of this protocol
/// it is the main custom module containing the other customization objects
public struct KBCustomization: KeemojiCustomizable {
    /// computed property for Configurator protocol implementation
    public var localizer: KeemojiLocalizer
        
        /// computed property for ImageProvider protocol implementation
        public var imageProvider: KeemojiImageProvider
        
        /// computed property for ColorPalette protocol implementation
        public var colorPalette: KeemojiColorPalette
            
        /// Initializes the KBCustomization() module, to be passed by injection
        /// the properties are initialized with the module's implementations for
        /// the respective protocols
        public init() {
            localizer = KMLocalizer()
            imageProvider = KMImageProvider()
            colorPalette = KMColorPalette()
        }

    }

    public struct KMImageProvider: KeemojiImageProvider {
        /// Retrieves an image asset for a given `name`, if an image exists
        /// To be used for app and keyboard assets
        /// ```
        /// image(named: "flag")
        /// ```
        ///
        /// - Parameter name: the name of the image asset
        ///
        /// - Returns: image asset for a given `name`, if an image exists;
        /// nil if no image exists with that name.
        public func image(named name: String) -> UIImage? {
            UIImage(named: name,
                    in: .main,
                    compatibleWith: nil)
        }

        /// Retrieves an icon asset for a given `name`, if an icon exists
        /// To be used in the context of the toolbar (for iconresources)
        /// ```
        /// icon(named: "flag")
        /// ```
        ///
        /// - Parameter name: the name of the icon asset
        ///
        /// - Returns: icon asset for a given `name`, if an icon exists;
        /// nil if no icon exists with that name.
        public func icon(named name: String) -> UIImage? {
            UIImage(named: name,
                    in: .main,
                    compatibleWith: nil)
        }
    }

    public struct KMColorPalette: KeemojiColorPalette {
      /// computed property for status bar background
      /// in case client has forced per-app appearance
      ///
      /// - Returns: The keyboard text color
      public var activation_status_bg: UIColor { .black }

        public init() {}

    }

    public struct KMLocalizer: KeemojiLocalizer {
        /// Produces the translation for the given `text` into the desired language.
        ///
        /// ```
        /// translation("Download") // "Descargar"
        /// ```
        ///
        ///
        /// - Parameter text: The text to be translated.
        ///
        /// - Returns: translation for the given `text` into the desired language.
        public func translation(_ text: String) -> String {
            NSLocalizedString(text, comment: "")
        }

        public init() {}

    }

Step 3.4: Deep linking

Add a URL Type with the name taken from custom Config key url_scheme to the URL types of the app.

Note: For the deep-links to work, they need to be implemented on the app client side.

Step 3.4: The keyboard activation can be done in two ways:

Either manually, by going to Settings and activating the keyboard or

By running the onboarding steps from the app (in that case it needs to be done after step 4).

Checkpoint: Verify that the keyboard is working properly

Step 4: Setup a bridge file for iOS

For iOS integration with React Native, a native module is required to facilitate communication between the JavaScript/TypeScript code and the native Swift/Objective-C code. TappaModule.swift will act as this bridge on the iOS platform.

Implement TappaModule.swift
Place the TappaModule.swift file in the appropriate directory within your iOS project (usually YourProjectName/ios/). This class should include the necessary methods to interact with your custom keyboard functionalities.

import Foundation
import Combine
import Keemoji

@objc(TappaModule)
class TappaModule: NSObject {
  private var cancellables: Set<AnyCancellable> = []
  private var rootController: UIViewController?
  
  @objc static let shared = TappaModule()
    
  private override init() { }

  // MARK: - interface
  @objc
  func initializeNativeSDK() -> Void {
    observeOnboardingStatus()
    KeyboardSDK.setup(customizable: KBCustomization())
    
    startup()
  }
  
  @objc
  func launchActivationIfNeeded(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: RCTPromiseRejectBlock) -> Void
  {
    Task {
      await MainActor.run {
        TappaModule.shared.startOnboarding()
      }
    }
    resolve("startOnboarding")
  }
  
  @objc
  func isKeyboardAdded(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: RCTPromiseRejectBlock)
  {
    resolve(KeyboardSDK.isKeyboardAdded())
  }
  
  @objc
  func isKeyboardInstalled(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: RCTPromiseRejectBlock)
  {
    resolve(KeyboardSDK.isKeyboardInstalled())
  }
  
  @objc
  func isOnboardingDismissed(
    _ resolve: @escaping RCTPromiseResolveBlock,
    rejecter reject: RCTPromiseRejectBlock)
  {
    resolve(KeyboardSDK.isOnboardingDismissed())
  }

  @objc func application(
    _ app: UIApplication,
    open url: URL,
    options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool
  {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
      _ = KeyboardSDK.application(open: url, options: options)
    }
    return true
  }

  @objc static func requiresMainQueueSetup() -> Bool {
    return true
  }
  
  // MARK: - private
  private func startup()
  {
    Task {
      await MainActor.run {
        KeyboardSDK.initialize()
      }
    }
  }
  
  private func startOnboarding() {
    if rootController == nil {
      rootController = UIApplication.shared.windows[0].rootViewController
    }
    KeyboardSDK.startOnboarding()
  }
  
  private func observeOnboardingStatus() {
    KeyboardSDK.onboardingStatus.$isOnboardingFinished
      .receive(on: DispatchQueue.main)
      .sink { [weak self] finished in
        guard finished else { return }
        guard let self = self else { return }
      
        self.closeOnboardingScreen()
      }
      .store(in: &cancellables)
    
    KeyboardSDK.onboardingStatus.$isOnboardingDismissed
      .receive(on: DispatchQueue.main)
      .sink { [weak self] dismissed in
        guard dismissed else { return }
        guard let self = self else { return }
      
        self.closeOnboardingScreen()
      }
      .store(in: &cancellables)
    
  }
  
  private func closeOnboardingScreen() {
    guard let rootVC = self.rootController  else { return }
    let appWindow = UIApplication.shared.windows[0]
    guard appWindow.rootViewController != rootVC else {return}
    appWindow.rootViewController = rootVC
    
    UIView.transition(
      with: appWindow,
      duration: 0.35,
      options: .transitionCrossDissolve,
      animations: nil,
      completion: nil)
  }
  
}

Expose the Module to React Native:
You need to expose TappaModule to React Native by using the RCT_EXTERN_MODULE and RCT_EXTERN_METHOD macros in an Objective-C file (e.g., TappaModuleBridge.m) that bridges your Swift code with React Native.

#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(TappaModule, NSObject)

RCT_EXTERN_METHOD(
                  launchActivationIfNeeded: (RCTPromiseResolveBlock)resolve
                  rejecter: (RCTPromiseRejectBlock)reject
                  )

RCT_EXTERN_METHOD(isKeyboardInstalled: (RCTPromiseResolveBlock)resolve
                  rejecter: (RCTPromiseRejectBlock)reject
                  )

RCT_EXTERN_METHOD(isKeyboardAdded: (RCTPromiseResolveBlock)resolve
                  rejecter: (RCTPromiseRejectBlock)reject
                  )

RCT_EXTERN_METHOD(isOnboardingDismissed: (RCTPromiseResolveBlock)resolve
                  rejecter: (RCTPromiseRejectBlock)reject
                  )

@end

Create the bridge header:

#ifndef Tappa_Bridging_Header_h
#define Tappa_Bridging_Header_h

#import <React/RCTBridgeModule.h>
#import <React/RCTViewManager.h>

#endif /* Tappa_Bridging_Header_h */

Use the Module in JavaScript/TypeScript:
Import and utilize the native module methods within your JavaScript or TypeScript code as needed.

import { NativeModules } from 'react-native';
const { TappaModule } = NativeModules;

Methods Exposed by TappaModule SDK

TappaModule provides several methods to interact with the native Android keyboard functionalities. Here's a breakdown of these methods and their usage:

  1. isKeyboardInstalled(): Promise
    Description: Checks if the keyboard is installed on the device.
    Returns: A Promise that resolves to a boolean value. true if the keyboard is installed, false otherwise.
    Usage Example:

  2. const installed = await TappaModule.isKeyboardInstalled();
    
  3. isKeyboardAdded(): Promise
    Description: Verifies whether the keyboard has been added to the input methods of the device.
    Returns: A Promise that resolves to a boolean value. true if the keyboard has been added, false otherwise.
    Usage Example:

  4. const added = await TappaModule.isKeyboardAdded();
    
  5. launchActivationIfNeeded(): void
    Description: Triggers the activation process for the keyboard if it is not already activated. This method should be called when the app detects that the keyboard is installed but not activated.
    Usage Example:

  6. TappaModule.launchActivationIfNeeded();
    

Integration in React Native Application

In your React Native application, these methods are used to manage the keyboard installation and activation states. The app checks the installation and activation status when it becomes active, and triggers the activation process if necessary.

  • Checking Installation Status: This is done using the checkInstallationStatus function, which calls isKeyboardInstalled and isKeyboardAdded to determine the current state of the keyboard installation and activation.
  • Handling State Changes: The app listens for state changes in the AppState and checks the installation status whenever the app returns to the active state. This ensures that the app always has the latest status.
  • Triggering Activation: If the keyboard is installed but not activated, launchActivationIfNeeded is called to start the activation process.
  • UI Response: Based on the installation and activation status, the app renders different screens - InstalledScreen if the keyboard is fully installed and activated, and NotInstalledScreen with an option to start the installation process otherwise.

Example Usage in App Component

useEffect(() => {
  const subscription = AppState.addEventListener(
    'change',
    async nextAppState => {
      if (nextAppState === 'active') {
        const {installed, added} = await checkInstallationStatus();

        if (added && !installed) {
          TappaModule.launchActivationIfNeeded();
        }
      }
    },
  );

  return () => {
    subscription.remove();
  };
}, []);

// ...rest of the component


What’s Next