React Native modules — Frameworks

Jan Verhoeven
7 min readDec 13, 2016

--

In the previous post I demonstrated how to bootstrap React Native modules with code implemented in Objective C and Java. Let’s expand on that and add an external library and compile that in. Let’s use the AWS library for iOS and Android. Amazon has done a partial migration to React Native, so some libraries still need to moved over. One of them is AWS Cognito Identity. The goal of this exercise is to be able to sign up with a fresh pair of credentials on both iOS and Android platform, with the javascript part of the code being identical!

It is assumed you executed the steps in the previous post before you got here. It also assumed you already made a user pool in the EU-WEST region.

iOS

First we add the iOS frameworks. Download the SDK for iOS. We only need the .framework files related to Cognito, including the Core library. Find them and copy them in a new folder named ‘Frameworks’ in the ios folder of the react-native-kickass-component.

The Framework files contain headers and binaries. We need to make them known in the project. We need to add two elements:

  1. The .h files need to be found by our module
  2. The .framework binaries need to be embedded in the compiled binary.

Let’s start with 1. Adapt the Framework search path to the module as $(SRCROOT)/Frameworks

Also add the framework files to the application as $(PROJECT_DIR)/../node_modules/react-native-kickass-component/ios/Frameworks

Step 2: Add the embedded binaries:

react-native-kickass-component/ios/RNKickassComponent.h (bold is new)

#import "RCTBridgeModule.h"
#import <AWSCore/AWSCore.h>
#import <AWSCognitoIdentityProvider/AWSCognitoIdentityProvider.h>
#import <AWSCore/AWSTask.h>
#import <AWSCore/AWSService.h>
@interface RNKickassComponent : NSObject <RCTBridgeModule>@end

react-native-kickass-component/ios/RNKickassComponent.m (bold is new)

#import "RNKickassComponent.h"@implementation RNKickassComponent@synthesize methodQueue = _methodQueue;typedef void (^ Block)(id, int);- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
RCT_EXPORT_MODULE(RNKickassComponent)-(instancetype)init{
self = [super init];
if (self) {
[AWSServiceConfiguration
addGlobalUserAgentProductToken:[NSString stringWithFormat:@"react-native-kickass-component/0.0.1"]];
}
return self;
}
RCT_EXPORT_METHOD(concatStr:(NSString *)string1
secondString:(NSString *)string2
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
resolve([NSString stringWithFormat:@"%@ %@", string1, string2]);
}
RCT_EXPORT_METHOD(signUp:(NSString *)region
userPoolId:(NSString *)userPoolId
clientId:(NSString *)clientId
email:(NSString *)email
password:(NSString *)password
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {

// Normally, you would do this only once, but for sake of example:

AWSServiceConfiguration *configuration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionEUWest1 credentialsProvider:nil];
[configuration addUserAgentProductToken:@"AWSCognitoIdentity"];
[AWSServiceManager defaultServiceManager].defaultServiceConfiguration = configuration;
AWSCognitoIdentityUserPoolConfiguration *userPoolConfiguration =
[[AWSCognitoIdentityUserPoolConfiguration alloc] initWithClientId:clientId
clientSecret:nil
poolId:userPoolId];
[AWSCognitoIdentityUserPool
registerCognitoIdentityUserPoolWithConfiguration:configuration
userPoolConfiguration:userPoolConfiguration forKey:@"UserPool"];
AWSCognitoIdentityUserPool *userPool = [AWSCognitoIdentityUserPool CognitoIdentityUserPoolForKey:@"UserPool"];

// Now signup:

AWSCognitoIdentityUserAttributeType * emailAttribute = [AWSCognitoIdentityUserAttributeType new];
emailAttribute.name = @"email";
emailAttribute.value = email;

//start a separate thread for this to avoid blocking the component queue, since
//it will have to comunicate with the javascript in the mean time while trying to signup
NSString* queueName = [NSString stringWithFormat:@"%@.signUpAsyncQueue",
[NSString stringWithUTF8String:dispatch_queue_get_label(self.methodQueue)]
];
dispatch_queue_t concurrentQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_CONCURRENT);

dispatch_async(concurrentQueue, ^{

[[userPool signUp:email password:password userAttributes:@[emailAttribute] validationData:nil]
continueWithBlock:^id _Nullable(AWSTask<AWSCognitoIdentityUserPoolSignUpResponse *> * _Nonnull task) {

if (task.exception){
reject([NSString stringWithFormat:@"Exception "],task.exception.reason, [[NSError alloc] init]);
}
if (task.error) {
reject([NSString stringWithFormat:@"%ld",task.error.code],task.error.description,task.error);
}
else {
// Return the username as registered with Cognito.
resolve(task.result.user.username);
}
return nil;
}];

});
}
@end

NOTE: If you not only want to publish react-native-kickass-component but also the ReactNativeKickassApp itself, you will need some small modifications (due to XCode always de-referencing links as created with npm link):

  1. Add react-native-kickass-component to package.json. Obviously you will need to have deployed react-native-kickass-component to npm first.
  2. Close XCode GUI.
  3. Start an editor and open ios/ReactNativeKickassApp.xcodeproj/project.pbxproj. Note that the project file is inside the xcodeproj file and you may need to go via the command line to edit it since it will not show up in the Finder.
  4. Find a section called /* Begin PBXFileReference section */ and edit every line that holds ../.. in its path and change it to ../node_modules, e.g.:

path = “../../react-native-kickass-component/ios/build/Debug-iphoneos/libRNKickassComponent.a”

into

path = “../node_modules/react-native-kickass-component/ios/build/Debug-iphoneos/libRNKickassComponent.a”

and save the file.

When you open XCode again, you should now see a path relative to ../node_modules instead of ../.. which is how it will be in a deployed version.

Android

We need to add some compile dependencies to react-native-kickass-component/android/build.gradle (bold is new):

dependencies {
compile 'com.facebook.react:react-native:0.20.+'
compile 'com.amazonaws:aws-android-sdk-core:2.2.+'
compile 'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.2.+'
}

react-native-kickass-component/src/main/java/com/reactlibrary/ RNKickassComponentModule/java (bold is new):

package com.reactlibrary;

import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AnonymousAWSCredentials;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.Regions;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserPool;
import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserAttributes;
import com.amazonaws.mobileconnectors.cognitoidentityprovider.handlers.SignUpHandler;
import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUser;
import com.amazonaws.mobileconnectors.cognitoidentityprovider.CognitoUserCodeDeliveryDetails;
import com.amazonaws.services.cognitoidentityprovider.AmazonCognitoIdentityProviderClient;


public class RNKickassComponentModule extends ReactContextBaseJavaModule {

private final ReactApplicationContext reactContext;

public RNKickassComponentModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

@Override
public String getName() {
return "RNKickassComponent";
}

@ReactMethod
public void concatStr(
String string1,
String string2,
Promise promise) {
promise.resolve(string1 + " " + string2);
}

@ReactMethod
public void signUp(
String region,
String userPoolId,
String clientId,
String email,
String password,
final Promise promise) {

AmazonCognitoIdentityProviderClient client = new AmazonCognitoIdentityProviderClient(new AnonymousAWSCredentials(), new ClientConfiguration());
// Sorry, hard coded as well.
client.setRegion(Region.getRegion(Regions.EU_WEST_1));
CognitoUserPool userPool = new CognitoUserPool(this.reactContext, userPoolId, clientId, null, client);
CognitoUserAttributes userAttributes = new CognitoUserAttributes();
userAttributes.addAttribute("email", email);
SignUpHandler signupCallback = new SignUpHandler() {
@Override
public void onSuccess(CognitoUser cognitoUser, boolean userConfirmed, CognitoUserCodeDeliveryDetails cognitoUserCodeDeliveryDetails) {
// Sign-up was successful
promise.resolve(cognitoUser.getUserId());
}

@Override
public void onFailure(Exception exception) {
// Sign-up failed, check exception for the cause
promise.reject("signup failed", exception.getMessage());
}
};
userPool.signUpInBackground(email, password, userAttributes, null, signupCallback);

};


}

Generic javascript

The great thing here is that the javascript part is identical for both Android and iOS.

react-native-kickass-component/index.js (bold is new):

import { NativeModules } from 'react-native';const component = NativeModules.RNKickassComponent;class KickassComponent {
constructor(region_id, user_pool_id, client_id) {
this.region_id = region_id;
this.user_pool_id = user_pool_id;
this.client_id = client_id;
}
async concatStr(string1, string2) {
return await component.concatStr(string1, string2);
}
async signUp(email, password) {
return await component.signUp(this.region_id, this.user_pool_id, this.client_id, email, password);
}

}
export default KickassComponent;

Let’s put the AWS config in a separate config ReactNativeKickassApp/awsconfig.js:

export default awsConfig = {
"region": "eu-west-1",
"user_pool_id": "YOUR_USER_POOL_ID",
"client_id": "YOUR_CLIENT_ID"
};

Note that the region is hard coded in the example modules (exercise left to the reader).

ReactNativeKickassApp/index.ios.js and index.android.js can both be the same (bold is new):

import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
TextInput,
Button
} from 'react-native';
import awsConfig from './awsconfig';import KickassComponent from 'react-native-kickass-component'export default class ReactNativeKickassApp extends Component {
constructor() {
super();
this.state = {
message: '',
email: '',
password: '',
signupUsername: '',
errorMessage:''
};
this.component = new KickassComponent(awsConfig.region, awsConfig.user_pool_id, awsConfig.client_id);
this.component.concatStr('Hello', 'world')
.then(message => this.setState({message: message}));
}
signup() {
this.component.signUp(this.state.email, this.state.password)
.then(signupUsername => this.setState({signupUsername: signupUsername, errorMessage: ''}))
.catch(errorMessage => this.setState({signupUsername: '', errorMessage: errorMessage.message}));
}
message() {
if (this.state.signupUsername === '' && this.state.errorMessage === '') {
return (<Text style={styles.label}>Enter email and password and press the button</Text>)
} else if (this.state.signupUsername !== '') {
return (<Text style={styles.label}>Signed up as {this.state.signupUsername} was succesfull</Text>)
} else {
return (<Text style={styles.label}>Error signing up {this.state.errorMessage}</Text>)
}
}
render() {
return (
<View style={styles.container}>
<Text />
{this.message()}
<Text style={styles.label}>E-mail address</Text>
<TextInput
style={styles.input}
onChangeText={(text) => this.setState({ email: text })}
value={this.state.email}
/>
<Text style={styles.label}>Password</Text>
<TextInput
style={styles.input}
secureTextEntry={true}
onChangeText={(password) => this.setState({ password: password })}
value={this.state.password}
/>
<Button style={styles.button} title="Signup" onPress={this.signup.bind(this)} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
label: {
flexDirection: 'row',
paddingHorizontal: 10,
paddingVertical: 5,
borderBottomWidth: 1,
borderBottomColor: '#EEE',
},
input: {
borderColor: 'gray',
paddingHorizontal: 10,
paddingVertical: 5,
borderWidth: 1,
marginLeft: 10,
marginRight: 10,
height: 40,
},
button: {
height: 40,
}
})
AppRegistry.registerComponent('ReactNativeKickassApp', () => ReactNativeKickassApp);

Now run either via react-native run-ios or react-native run-adroid. It should show a UI to enter credentials and signup.

Using the library in a new project

Please note since we are using frameworks that are not included in the NPM repository, some extra work needs to be done:

  1. Add ‘react-native-kickass-library’ to the package.json (obviously)
  2. The .framework files mentioned above should be copied into the node_modules/react-native-kickass-component/ios/Frameworks/ folder.
  3. Execute ‘react-native link’ so the package is included in iOS and Android.
  4. The ‘Framework path’ in the Libraries section of the new project needs to be set. [This needs to be investigated why this is. It should already be ok due to being included in the ‘react-native-kickass-component’.]
  5. The .framework files need to be added to the project ‘Embedded Binaries’. [This needs some investigation as well since this should not be needed for the same reason as point 4].

[Note from author: I will be happy to learn from someone why the problems mentioned above occur.]

--

--

Jan Verhoeven

Intents to write a bit more on all the things that have his interest. Be ready for totally random topics.