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 Cashback Keyboardard 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:
- 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:
- 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.
- 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.
- 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.
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:
-
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: -
const installed = await TappaModule.isKeyboardInstalled();
-
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: -
const added = await TappaModule.isKeyboardAdded();
-
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: -
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 callsisKeyboardInstalled
andisKeyboardAdded
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 theactive
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, andNotInstalledScreen
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
Updated 3 months ago