Reading Appcelerator Titanium app properties in React Native

Titanium’s Ti.App.Properties can be read by React Native.

I recently worked on a React Native rewrite of an Appcelerator Titanium app. To provide a seamless update for our users, we needed to maintain app properties —in our case, authentication tokens — after they upgraded to the shiny new React Native build.

The Titanium docs state that properties are in iOS are held in NSUserDefaults plist files, and Android properties are in a SharedPreferences file: /data/data/com.domain.app/shared_prefs/titanium.xml

Android

Kevin Leung’s handy react-native-default-preference module gives access to both iOS NSUserDefaults and Android SharedPreferences.

For Android then, getting your Titanium properties is straightforward:

await DefaultPreference.setName('titanium');
const setting = await DefaultPreference.get('setting');

iOS

My hope was that the code above would work without modification for iOS; nice short article, job done. No such luck, unfortunately. All combinations of DefaultPreference.setName with “titanium”, “Titanium”, “tiapp”, “com.domain.app” (and nothing at all) proved fruitless for me on iOS.

Using react-native-fs to scan the filesystem, the plist of interest was located at /var/mobile/Containers/Data/Application/[GUID]/Library/Preferences/com.domain.app.plist. However, attempting to read this file directly resulted in an “Error — Invalid continuation byte”

When plists are stored as text files, they can be read directly, but binary plists (like my one above) need extra work. To convert a plist to a string, credit to Dreamlax, who posted a working snippet of Objective-C code that could do just that.

Using that code as a base, here is a Native Module that will read a raw plist that can be dropped in to your Xcode project (extra credit due to Oskar Groth for code to convert an NSException to NSError for the promisified version).

Once the plist contents are in a string, Nathan Rajlich’s plist npm library can convert it into an object: although you may find, as I did, that you need to grab the parser file and use a copy of that rather than add the plist library, which conflicts with an older version used by xcode brought in by React Native.

Finally, we’re in a position to write a cross-platform ES6 function that will retrieve Titanium’s Ti.App.Properties, with usage signature:

const setting = await titaniumProperty('com.domain.app', 'setting');

titaniumProperty.js

import { Platform, NativeModules } from 'react-native';
import DefaultPreference from 'react-native-default-preference';
import RNFS from 'react-native-fs';
import plist from './parsePlist'; // github.com/TooTallNate/plist.js

let initialisePromise;
let iOSPlist = {};

const initialisation = async (bundle) => {
if (!initialisePromise) {
initialisePromise = new Promise(async (resolve, reject) => {
if (Platform.OS === 'ios') {
const plistFilename = `${RNFS.LibraryDirectoryPath}/Preferences/${bundle}.plist`;
const plistAsString = await NativeModules.ReadPlist.readRawPlistPromise(plistFilename);
iOSPlist = plist.parse(plistAsString);
} else if (Platform.OS === 'android') {
await DefaultPreference.setName('titanium');
}
resolve();
});
}
return initialisePromise;
}

export default async function(bundle, setting) {
await initialisation(bundle);
if (Platform.OS === 'ios') {
return Promise.resolve(iOSPlist[setting]);
} else if (Platform.OS === 'android') {
return DefaultPreference.get(setting);
}
return Promise.resolve(undefined);
}

(For the above to work, your bundle identifier must remain the same, and you must use the same signing certificates and keystores to bundle your React Native app as you used for Titanium)