Working with env variables in React Native for iOS app - react-native

I'm having a heck of a time getting env variables to work correctly for a React Native app. I develop locally testing on web and Expo, and then my aim is to publish to the App Store.
At first, I tried using react-native-dotenv, but while it seemed fine locally, prod settings wouldn't take hold when I did a formal Expo build to run either on iOS Simulator or submit to App Store.
So I abandoned that and went with a suggestion from the Expo documentation around creating a Config.js file, as so:
const { manifest } = Constants;
if (Platform.OS == 'web') {
if (Updates.channel === 'production') {
Config.ENV_NAME = 'production';
Config.BACKEND_URL = '[...]';
Config.HIDDEN_FEATURES = false;
} else if (Updates.channel === 'preview') {
Config.ENV_NAME = 'preview'
Config.BACKEND_URL = '[...]';
Config.HIDDEN_FEATURES = false;
} else if (Updates.channel === 'development') {
Config.ENV_NAME = 'development';
Config.BACKEND_URL = 'http://localhost:3000/';
Config.HIDDEN_FEATURES = true;
}
} else if (Platform.OS == 'ios') {
[Repeat of above, but with different values as needed]
}
Elsewhere in my app, I would try to use this like so:
import Config from '../Config';
console.log("Backend URL: " + Config.BACKEND_URL + ", env: " + Config.ENV_NAME)
The problem is - this didn't seem to work at all! Any of the Config keys just didn't yield anything. When trying to debug this, I got other parts of the code to log Updates.channel and looks like that wasn't working anywhere (neither web nor ios), so nothing got set.
Questions...
Is there a solid recommended way to do env variables in React Native for an iOS app? I've seen so many different choices but hard to tell if one's the most common / recommended
Am I doing something wrong with the attempt to use Config.js above?

Related

Expo API for app updates resulting in blank white screen

I’m using Expo in the managed workflow. The app is coming along and I’ve added the ability for the app to auto-update itself. I’m using the expo-updates library: https://docs.expo.dev/versions/latest/sdk/updates and the implementation is pretty straightforward.
When our app is opened, our app’s App.tsx runs Updates.checkForUpdateAsync() and then Updates.reloadAsync(). We throw up a brief spinner and 9 out of 10 times the update works just fine. But about 1 out of 10 times the app screen goes white. The iOS header bar with time and Wi-Fi is still visible, but everything else is white. The user has to kill the app and re-open it to resume and now they’re running the latest version.
Unfortunately, this is a difficult issue to reproduce. We can’t do updates in development so we are trying to debug this in our staging environment (a mirror of production). Our repro steps are to push a trivial update to Expo, when the build completes, relaunch the app. We have to do it about ~10 times before we’ll get a freeze case.
The key code is: App.tsx runs:
newUpdate = await checkForAppUpdate()
If (newUpdate) await forceUpdate()
And then our library for these two methods is simple:
import * as Updates from 'expo-updates'
import {Platform} from "react-native"
export const checkForAppUpdate = async (): Promise<boolean> => {
if (Platform.OS == "web" || __DEV__) return false
try {
const update = await Updates.checkForUpdateAsync()
return update.isAvailable
} catch (e) {
console.error(e)
return false
}
}
export const forceUpdate = async () => {
try {
const result = await Updates.fetchUpdateAsync()
if (result.isNew) {
await Updates.reloadAsync()
}
} catch (e) {
console.error(e)
}
}
I’m at a loss for how to debug this. Any clues as to what might be going on? I’m assuming a race condition given the sporadic behavior but I’m stuck.
I ended up figuring this out, but it was tricky. I added all sorts of additional logging to try and catch any exception being thrown and there was not one. The issue is that I had a new dependency in my Expo build/bundle which was not yet supported by the binary version of my app I was running. I rebuilt my binary with the latest libraries and this started working again.
So anyone else who might be faced with this issue: do an audit of all library dependencies with your app and roll back to a previous version when updating was working to isolate what changed.

Unifying localhost dev api server access for expo app across Android, IOS, and web?

I'm setting up a simple React Native learning app for several students on Expo, that also talks to an API server the student is learning to code.
The student's API server is run via node server.js, and serves on localhost:3000 on the student's machine. It has nothing to do with expo.
I want students to be able to run their app via any of expo start --android, expo start --ios, or expo start --web, on the same machine that runs their API server. Each student runs from home on a different home wifi network, and doesn't necessarily know the ins and outs of ip addresses or networking.
When using expo start --web, we get CORS exceptions, unless we use the custom webpack.config.js work around (first create webpack.config.js via https://docs.expo.io/guides/customizing-webpack/, then put this in webpack.config.js):
const createExpoWebpackConfigAsync = require('#expo/webpack-config');
module.exports = async function(env, argv) {
const config = await createExpoWebpackConfigAsync(env, argv);
if (config.mode === 'development') {
config.devServer.proxy = {
'/**': {
target: {
host: 'localhost',
protocol: 'http:',
port: 3000,
},
secure: false,
changeOrigin: true,
logLevel: 'info',
},
};
}
return config;
};
This is great, because we can make api calls to ./end/point without knowing the student's ip address, and the webpack devServer launched by expo-cli effectively proxies around to http://localhost:3000/end/point on the student's development machine.
Meanwhile, for iOS and Android, I've found this snippet:
import Constants from "expo-constants";
const { manifest } = Constants;
const SERVER_URL = "http://"+manifest.debuggerHost.split(`:`).shift().concat(`:3000`)+"/";
and then using SERVER_URL when using fetch().
But, we're missing a unified solution that works agnostic of which environment we're in (web, ios, or android). The webpack proxy only appears to be on and work when using the expo web client (expo-cli doesn't launch webpack for ios or android), and the 2nd option (A) doesn't work out of the box on web and (B) would trigger a CORS exception anyway.
How can I elegantly write one bit of code, or otherwise set up the project for the students, so that (A) they don't need to know their dev machine's ip address, or what that means and (B) it will work regardless of whether they're in the web, android, or ios expo client?
Don't like this as an answer and would prefer someone who knows better to point out better, but this is what I ended up using that seems to work, at least in development:
// Some chatter that Contants.manifest needs to come from a different package?
import Constants from "expo-constants";
const { manifest } = Constants;
const SERVER_URL = (() => {
// TODO - put a "prod" api server somewhere
// Android / IOS - no CORS issue.
if (!!manifest.debuggerHost) {
return "http://"+manifest.debuggerHost.split(`:`).shift().concat(`:3000/`);
}
// Expo Web client, making use of webpack.config.js (see original question) for devServer proxy.
else {
return "./";
}
})();
...
fetch(SERVER_URL + 'some_endpoint/').then(...)

How to get Expo static deep link for development?

I need an Expo static deep link for development to use for Oauth redirect with a 3rd party ( Cognito )
I have used Linking.makeUrl() but this returns a deep link with a dynamic local ipaddress
exp://10.0.0.107:19000 that will not be consistent for other developers on the team.
The documentation at:
https://docs.expo.io/versions/latest/workflow/linking/#linking-module
Says the various environment links look like
Published app in Expo client : exp://exp.host/#community/with-webbrowser-redirect
Published app in standalone : myapp://
Development : exp://wg-qka.community.app.exp.direct:80
I have tried that Development link but it fails to open.
I have the similar issue too, here is my solution
Also post in https://github.com/aws-amplify/amplify-js/issues/4244#issuecomment-586845322
In case anyone still needs help for expo+amplify+social logins
app.json
{
"expo": {
"scheme": "exposchemeappname://" // use any name you like, just make it unique
}
}
App.js
import { Linking } from 'expo';
import * as WebBrowser from 'expo-web-browser';
import awsconfig from './aws-exports';
const amplifyConfig = {
...awsconfig,
oauth: {
...awsconfig.oauth,
urlOpener: async (url, redirectUrl) => {
// On Expo, use WebBrowser.openAuthSessionAsync to open the Hosted UI pages.
const { type, url: newUrl } = await WebBrowser.openAuthSessionAsync(url, redirectUrl);
if (type === 'success') {
await WebBrowser.dismissBrowser();
if (Platform.OS === 'ios') {
return Linking.openURL(newUrl);
}
}
},
options: {
// Indicates if the data collection is enabled to support Cognito advanced security features. By default, this flag is set to true.
AdvancedSecurityDataCollectionFlag: true
},
}
};
const expoScheme = "exposchemeappname://"
// Technically you need to pass the correct redirectUrl to the web browser.
let redirectUrl = Linking.makeUrl();
if (redirectUrl.startsWith('exp://1')) {
// handle simulator(localhost) and device(Lan)
redirectUrl = redirectUrl + '/--/';
} else
if (redirectUrl === expoScheme) {
// dont do anything
} else {
// handle the expo client
redirectUrl = redirectUrl + '/'
}
amplifyConfig.oauth.redirectSignIn = redirectUrl;
amplifyConfig.oauth.redirectSignOut = redirectUrl;
Amplify.configure(amplifyConfig);
Make sure you add the following redirect urls to amplify amplify auth update
# development
exp://127.0.0.1:19000/--/
exp://192.168.1.101:19000/ # depends on your lan ip
# expo client
exp://exp.host/#[EXPO_ACCOUNT]/[EXPO_APPNAME]/
# expo scheme for standalone
exposchemeappname://
you can use Linking module
install it by running: expo install expo-linking
import it at the top of your file: import * as Linking from "expo-linking";
and then use: Linking.makeUrl(); to get the link to your app hosted by expo client
console it to see the url

Check if build was sideloaded or downloaded from App/Play Store

React Native uses __DEV__ internally to check whether an app is a dev or release build.
We use that to determine whether we should point to our staging or production environments.
_host = (__DEV__) ? 'https://staging-api.foo-app.com' : 'https://api.foo-app.com';
if (Platform.OS === 'ios') {
deploymentKey = (__DEV__) // iOS
? '5eCkg3JX3aip-D_a77eea5c3-0MXihVlUTZ4yy45a-432a-b73e-0a844d8b8310' // Staging
: 'zGxOja-Yhchs87eea5c3-0d5a-432aQriLlV17gI-sdj55-b73e-0a844d8b8310'; // Production
} else {
deploymentKey = (__DEV__) // Android
? 'vrrKTaq08Hid77eea5c3-0d5a-432aDhXbdI8-G9CnWmqc-b73e-0a844d8b8310' // Staging
: '8DclNAKdcQkKlQDL77eea5c3-0d5a-432aslW1SeS6sDMo-b73e-0a844d8b8310'; // Production
}
The problem is that __DEV__ evaluates to false for any builds sideloaded to a device from XCode and Android Studio. So, to test on a device, we do this in a few places:
// _host = (__DEV__) ? 'https://staging-api.foo-app.com' : 'https://api.foo-app.com';
_host = 'https://staging-api.foo-app.com';
How can we determine if an app is sideloaded vs downloaded from the App Store or Play Store?
Instead of having keys and data that switches based on __DEV__...might I suggest using various .env files using a, much safer, 12-factor approach with react-native-config.
This way you can have deployment keys and environment based variables within files that can be .gitignored.
You can have something like:
.env (Staging)
HOST=https://staging-api.foo-app.com
DEPLOYMENT_KEY_IOS=5eCkg3JX3aip-D_a77eea5c3-0MXihVlUTZ4yy45a-432a-b73e-0a844d8b8310
DEPLOYMENT_KEY_ANDROID=vrrKTaq08Hid77eea5c3-0d5a-432aDhXbdI8-G9CnWmqc-b73e-0a844d8b8310
.env.production (Production)
HOST=https://api.foo-app.com
DEPLOYMENT_KEY_IOS=vrrKTaq08Hid77eea5c3-0d5a-432aDhXbdI8-G9CnWmqc-b73e-0a844d8b8310
DEPLOYMENT_KEY_ANDROID=8DclNAKdcQkKlQDL77eea5c3-0d5a-432aslW1SeS6sDMo-b73e-0a844d8b8310
react-native-config instructions should be clear enough on how you'd use each file based off build type.

Using react native with Optimizely

I try to follow documentation in Optimizely to get my react native app (#22.2) working but getting such bug.
MainActivity.java:24: error: cannot find symbol
Optimizely.startOptimizelyWithApiToken("xxxxxx", getApplication());
^
symbol: method startOptimizelyWithApiToken(String,Application)
location: class Optimizely
1 error
:app:compileDebugJavaWithJavac
What is wrong and how can I debug . I try
adb logcat ReactNative:V ReactNativeJS:V
but it's not giving me any information
I an on the engineering team at Optimizely and we've released a brand new product called FullStack that is more geared towards developers. As part of the product we now offer a JavaScript SDK for running experiments in all JavaScript clients, including React Native.
To use you would install our SDK:
npm install optimizely-client-sdk
And then you can split traffic using our activate and track methods.
Here is an example:
var optimizely = require('optimizely-client-sdk');
// Initialize an Optimizely client
var optimizelyClientInstance = optimizely.createInstance({ datafile: datafile });
// ALTERNATIVELY, if you don't use CommonJS or npm, you can install the minified snippet and use the globally exported varible as follows:
var optimizelyClientInstance = window.optimizelyClient.createInstance({ datafile: datafile });
// Activate user in an experiment
var variation = optimizelyClientInstance.activate("my_experiment", userId);
if (variation === 'control') {
// Execute code for variation A
} else if (variation === 'treatment') {
// Execute code for variation B
} else {
// Execute default code
}
// Track conversion event
optimizelyClientInstance.track("my_conversion", userId);
For more information please checkout our developer docs: https://developers.optimizely.com/x/solutions/sdks/introduction/index.html?language=javascript
i sorted problem is more about reading docs and using legacy:
compile ('com.optimizely:optimizely-legacy:+#aar') {
transitive = true
}
and then:
Optimizely.startOptimizely("xxxx", getApplication());