rct-fh

React Native Feedhenry Wrapper (unofficial) ============= **React Native Wrapper around Feedhenry SDK** is built to provide a RN App access to both **iOS** and in the future also for **Android** to the Feedhenry SDK. You'll find all the information relat

Usage no npm install needed!

<script type="module">
  import rctFh from 'https://cdn.skypack.dev/rct-fh';
</script>

README

React Native Feedhenry Wrapper (unofficial)

React Native Wrapper around Feedhenry SDK is built to provide a RN App access to both iOS and in the future also for Android to the Feedhenry SDK. You'll find all the information related to documentation of the Objective-C SDK this module is based on here.

Why a native bridge? Why not just JavaScript?

As of today plain JS SDK is browser driven and hence not usable in React Native.

Is there a React Native template?

Indeed, you can find it here.

Contents

  1. Create your React Native Project
  2. Add 'rct-fh' dependency
  3. Let's link the new module
  4. Install RHMAP iOS framwork using cocoapods
  5. Create the RHMAP iOS configuration file
  6. Using the module

1. Create your React Native Project

Do it as usual, for example: $ react-native init Test001

Next an example output of the previous command.

$ react-native init Test001
This will walk you through creating a new React Native project in /Users/u001/Projects/react/rhmap-lib/Test002
Using yarn v0.17.8
Installing react-native...
yarn add v0.17.8
info No lockfile found.
[1/4] 🔍  Resolving packages...
warning react-native > xcode > node-uuid@1.4.7: Use uuid module instead
[2/4] 🚚  Fetching packages...
...
├─ wordwrap@1.0.0
├─ xml-name-validator@2.0.1
└─ yargs@6.6.0
✨  Done in 4.21s.
To run your app on iOS:
   cd /Users/u001/Projects/react/rhmap-lib/Test001
   react-native run-ios
   - or -
   Open ios/Test002.xcodeproj in Xcode
   Hit the Run button
To run your app on Android:
   cd /Users/u001/Projects/react/rhmap-lib/Test001
   Have an Android emulator running (quickest way to get started), or a device connected
   react-native run-android

2. Add 'rct-fh' dependency

Using npm: npm install rct-fh

Or using yarn yarn add rct-fh

Underneath the output using yarn.

$ yarn add rct-fh
yarn add v0.17.8
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
warning Incorrect peer dependency "react@^15.5.0".
[4/4] 📃  Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
└─ rct-fh@0.0.9
✨  Done in 3.50s.

3. Let's link the new module

To do so, let's use: react-native link as in the following test.

$ react-native link
Scanning 564 folders for symlinks in /Users/u001/Projects/react/rhmap-lib/Test001/node_modules (5ms)
rnpm-install info Linking rct-fh ios dependency 
rnpm-install info iOS module rct-fh has been successfully linked 

4. Install RHMAP iOS framwork using cocoapods

Let's create a Podfile at ./ios/Podfile, change Test001 by the name of the React Native project us used with react-native init.

source 'https://github.com/CocoaPods/Specs.git'
project 'Test001.xcodeproj'
platform :ios, '7.0'
use_frameworks!
target 'Test001' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # use_frameworks!
  # Pods for FH
  #pod 'FH', '~> 3.1.1'
  pod 'RCTFH', path: '../node_modules/rct-fh'
end

Now change dir to ./ios and run pod install

You should get an output similar to this one.

$ pod install

---------------------------------------------
Error loading the plugin `cocoapods-appledoc-0.1.0`.

Gem::LoadError - Unable to activate cocoapods-appledoc-0.1.0, because cocoapods-1.2.0.beta.1 conflicts with cocoapods (~> 0.34)
/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/rubygems/specification.rb:2007:in `raise_if_conflicts'
/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/lib/ruby/2.0.0/rubygems/specification.rb:1176:in `activate'
/Library/Ruby/Gems/2.0.0/gems/claide-1.0.1/lib/claide/command/plugin_manager.rb:93:in `safe_activate_and_require'
/Library/Ruby/Gems/2.0.0/gems/claide-1.0.1/lib/claide/command/plugin_manager.rb:31:in `block in load_plugins'
/Library/Ruby/Gems/2.0.0/gems/claide-1.0.1/lib/claide/command/plugin_manager.rb:30:in `map'
/Library/Ruby/Gems/2.0.0/gems/claide-1.0.1/lib/claide/command/plugin_manager.rb:30:in `load_plugins'
/Library/Ruby/Gems/2.0.0/gems/claide-1.0.1/lib/claide/command.rb:326:in `block in run'
/Library/Ruby/Gems/2.0.0/gems/claide-1.0.1/lib/claide/command.rb:325:in `each'
/Library/Ruby/Gems/2.0.0/gems/claide-1.0.1/lib/claide/command.rb:325:in `run'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0.beta.1/lib/cocoapods/command.rb:50:in `run'
/Library/Ruby/Gems/2.0.0/gems/cocoapods-1.2.0.beta.1/bin/pod:55:in `<top (required)>'
/usr/local/bin/pod:23:in `load'
/usr/local/bin/pod:23:in `<main>'
---------------------------------------------

Analyzing dependencies
Fetching podspec for `RCTFH` from `../node_modules/rct-fh`
Downloading dependencies
Installing AeroGear-Push (1.2.0)
Installing FH (3.1.1)
Installing RCTFH (0.1.0)
Installing Reachability (3.2)
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `Test001.xcworkspace` for this project from now on.
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 4 total pods installed.

5. Create the RHMAP iOS configuration file

Below you'll find an example of fhconfig.plist file.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist>
<dict>
  <key>host</key>
  <string>https://cvicensa.us.demos.redhatmobile.com</string>
  <key>appid</key>
  <string>your_appid</string>
  <key>projectid</key>
  <string>your_projectid</string>
  <key>appkey</key>
  <string>your_appkey</string>
  <key>connectiontag</key>
  <string>x.y.z</string>
</dict>
</plist>

To create it open the XCode workspace generated before (if you cannot find the workspace maybe you should go to the previous chapter and run pod install).

$ cd Test001
$ open ios/Test001.xcworkspace/

Once inside the XCode workspace create a new file under the project node in the file inspector.

  1. Right click on the project icon in the file inspector and select 'New File'. Select 'Property list' as the file type.
  2. Give the file the following name fhconfig.plist
  3. It's time to copy the contents of the sample file above, to do so, right click the file and select 'Open As'→'Source Code'
  4. Paste the contents and edit the file to match your Cloud App where is running the sample 'hello' end point.

6. Using the module

Below you'll find an example of index.ios.js that uses our module rct-fh. Please pay attention to the class name exported (Test001 in our example) and also to the name of the app registered in the last line (again Test001 in our example). For simplicity make the name of both the class and the component registered to be the name of the React Native application we used in step #1 where we run react-native init <App Name>.

Importing the module

To import the module, just require 'rct-fh'.

var RCTFH = require('rct-fh');

The excerpt below shows how we're defining an object exposing native module methods asynchronously. See index.js for more details.

var RCTFH = {
  init: async function() {
    return await FH.init();
  },
  auth: async function(authPolicy, username, password) { 
    return await FH.auth(authPolicy, username, password);
  },
  cloud: async function(options) { 
    return await FH.cloud(options);
  }
};

Init the module

The function to initialize the module is: RCTFH.init(). This function is asynchronous and as such we could use the keyword await to asynchronously await for the init process to finish (in the same fashion as then in a Promise). As you can see below, once the init process has resolved properly we get the result object. On the other hand if there is a problem while initializing the module the init process will be rejected and hence the catch code will be fired. See method RCT_REMAP_METHOD(init, resolver, rejecter) RCTFH.m.

try {
    this.setState({message: 'Initializing...'});
    const result = await RCTFH.init();
    console.log('init result', result);
    this.setState({message: 'Ready'});
    
    if (result === 'SUCCESS') {
      console.log('SUCCESS');
      this.setState({init: true});
    } else {
      console.error('Error');
    }
} catch (e) {
    console.error('Exception', e);
} 

Authentication

Before we can use this function we need to have defined an authentication policy in RHMAP Studio. For more information about authentication policies please go to RHMAP Auth Policies.

The function to trigger an authentication policy is: RCTFH.auth(). This function is also asynchronous and as such we could use the keyword await to asynchronously await for the authentication process to finish. If the policy is invoked properly we will get a result object, if the credentials provided are correct the object will include a sessionToken attribute. On the other hand if there is a problem the function will be rejected and hence the catch code will be fired. See method RCT_REMAP_METHOD(auth, authPolicy, username, password, resolver, rejecter) RCTFH.m.

try {
    const result = await RCTFH.auth(authPolicy, username, password);
    if (typeof result.sessionToken !== 'undefined') {
      console.log('AUTHENTICATED');
    } else {
      console.log('UNAUTHENTICATED');
    }
} catch (e) {
    console.error('ERROR', e);
}

REST call, 'cloud' API

The function to call a RESTful endpoint exposed in a FeedHendry Cloud App is: RCTFH.cloud(options). Again, this function is asynchronous and as such we could use the keyword await to asynchronously await for the cloud call process to finish. In the same fashion as the init call, once the cloud call has resolved properly we get the result object and if there is a problem the catch code will be fired. See method RCT_REMAP_METHOD(cloud, options, resolver, rejecter) RCTFH.m.

As you can see, we are using a set options to use this function:

  • path cloud app endpoint
  • method HTTP method; GET, POST, etc.
  • contentType usually application/json
  • data object we want to use along with the HTTP method, in the case of the GET method a flat object is turned into query parameters
  • timeout HTTP timeout
try {
  const result = await RCTFH.cloud({
    "path": "/hello", //only the path part of the url, the host will be added automatically
    "method": "GET", //all other HTTP methods are supported as well. For example, HEAD, DELETE, OPTIONS
    "contentType": "application/json",
    "data": { "hello": this.state.userInput}, //data to send to the server
    "timeout": 25000 // timeout value specified in milliseconds. Default: 60000 (60s)
  });

  if (result && result.msg)
    this.setState({message: result.msg});
  else
    this.setState({message: JSON.stringify(result)});
} catch (e) {
  this.setState({message: 'Error' + e});
}

# index.ios.js complete example code

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  Text,
  TextInput,
  View,
  Button,
  Image
} from 'react-native';

var RCTFH = require('rct-fh');

export default class Test001 extends Component {
   constructor(props) {
    console.log('constructor()');
    super(props);

    this.state = {
      message: 'Waiting...',
      userInput: '',
      init: false
    };
  }

  componentDidMount () {
    // Let's init RHMAP module after the component mounts
    this.init();
  }


  sayHello = async () => {
    console.log('sayHello');
    try {
      const result = await RCTFH.cloud({
        "path": "/hello", //only the path part of the url, the host will be added automatically
        "method": "GET", //all other HTTP methods are supported as well. For example, HEAD, DELETE, OPTIONS
        "contentType": "application/json",
        "data": { "hello": this.state.userInput}, //data to send to the server
        "timeout": 25000 // timeout value specified in milliseconds. Default: 60000 (60s)
      });

      console.log('sayHello result', result);
      if (result && result.msg)
        this.setState({message: result.msg});
      else
        this.setState({message: JSON.stringify(result)});
    } catch (e) {
      this.setState({message: 'Error' + e});
    }
  }

  init = async () => {
      try {
        this.setState({message: 'Initializing...'});
        const result = await RCTFH.init();
        console.log('init result', result);
        this.setState({message: 'Ready'});

        if (result === 'SUCCESS') {
          console.log('SUCCESS');
          this.setState({init: true});
        } else {
          console.error('Error');
        }
      } catch (e) {
        console.error('Exception', e);
      }  
  }

  updateUserInput = async (userInput) => {
    this.setState({userInput: userInput});
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.h1}>
          Feed Henry - React Native Template
        </Text>
        <TextInput key='2' style={styles.input} autoCapitalize = 'none'
          onSubmitEditing={(event) => this.updateUserInput(event.nativeEvent.text)}
          onEndEditing={(event) => this.updateUserInput(event.nativeEvent.text)}
          placeholder='Enter Your Name Here'
          placeholderTextColor='grey'
        />
        
        <Button style={styles.button}
        disabled={!this.state.init}
        onPress={this.sayHello}
        title="Say Hello From The Cloud"
        accessibilityLabel="Say Hello From The Cloud"
        />
        
        <View style={{flex: 1, flexDirection: 'row', alignItems: 'flex-start'}}>
        <Text style={styles.message}>
        {this.state.message}
        </Text>
        </View>

      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    paddingTop: 23,
    flex: 1,
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
  },
  row: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
  },
  input: {
      margin: 30,
      height: 36,
      padding: 4,
      fontSize: 18,
      borderWidth: 1,
      borderColor: 'black',
      borderRadius: 8,
      color: 'black'
   },
  h1: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  message: {
    flex: 1,
    height: 150,
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
    color: 'grey',
    borderWidth: 1,
    borderColor: 'grey',
  },
});

AppRegistry.registerComponent('Test001', () => Test001);