Expokit full local build without publish - android

My company is switching to expo react-native mobile apps. We developed them in expo and ejected them. From here we wanted standalone apps without OTA and with Offline support. We also did not want to publish anything to expo for security reasons and may go to self hosting in the future.

For the time being we want to build IPAs and APKs that are self contained without ever doing expo publish, as we do not want to publish to expo nor a self hosted space at this time.

We noticed that the expo bundle-assets requires a connection to either the expo server or the self hosted server. To get around this we copied the files directly for IOS from the dist.

We got IOS working without ever doing expo publish by the following steps:

  • Develope in expo
  • Expo export
  • Expo eject
  • Copy bundle, manifest, and assets from the expo export dist folder into the app in xcode by adding to the bundle expo assets build phase.

However our problem is that we cannot figure out how to do the same for android. Again, we want to not have to do expo publish. We tried starting a local server on our local machine and point to that as our server, however that requires a cert as it’s https.

Currently we are looking at adding embedded responses in AppConstants.java.

Which get us further but still get the error Expo encountered a fatal error: java.lang.NullPointerException: Attempt to invoke virtual method 'java.io.InputStream okhttp3.ResponseBody.byteStream()' on a null object reference

We thought that this may have been a feature somewhere but we can’t find it.

  • Least Restrictive - publish to expo
  • Restrictive - publish to self hosted
  • Most Restrictive - everything is handled locally at build time

Any help with handling everything locally for android would be appreciated.

We could not find an answer to this so we are going to post ours for both IOS and Android. A lot of the threads about this got close but ended or were not exactly what we wanted. What we wanted was to make 100% standalone apps that never leaves a single computer until you put the app into the store.

We wanted this for security reasons and did not wish to pursue self hosting options at this time. We hope this becomes a feature in the future.

Note: With the following we also have OTA turned off. We have not tried this method with OTA yet.

Build apps without `expo publish`
    IOS
        Develope in expo
        Expo export (any url)
            rename asset files in ./dist/assets to corisponding name inside ./dist/ios-index.json #bundledAssets. IE `3cf9a9d2c197ce57948904c499e0fa79` -> `asset_3cf9a9d2c197ce57948904c499e0fa79.png`
            rename bundle and manifest inside ./dist/ to shell-app-manifest.json and shell-app.bundle respectivly 
                note it's .bundle not .js
        expo eject
            expokit
        copy renamed bundle and manifest to `./ios/myapwuhp/Supporting`
        open in xcode (use workspace file from export not project)
            in project navigator click on your project, go to build phases -> `Bundle Expo Assets` -> add in `cp /Users/...path.../dist/assets/ "$dest"` after the `expo bundle-assets --platform ios --dest "$dest"`
                this will overwrite the assets they already tried to add from what was published (if you have not published before, which is the point of this, the files will be xml files about )
            switch build to release
        test app in xcode without the expo packager running (no `expo start`)
        Distribute app like a normal app


    Android
         Develope in expo
        Expo export (any url)
            rename asset files in ./dist/assets to corisponding name inside ./dist/ios-index.json #bundledAssets. IE `3cf9a9d2c197ce57948904c499e0fa79` -> `asset_3cf9a9d2c197ce57948904c499e0fa79.png`
            rename bundle and manifest inside ./dist/ to shell-app-manifest.json and shell-app.bundle respectivly 
                note it's .bundle not .js
        expo eject
            expokit
        copy renamed bundle and manifest to `android/app/src/main/assets`
        open Android Studio and `import project (gradle)` and select `./android/`
        copy the following files from the dist folder to `./android/app/src/main/assets`
             Bundle from dist/bundle
             renamed keystore in dist
             renamed assets in dist/assets
         add in lines to `./android/app/src/main/java/host/exp/exponent/generated/AppConstants.java`
             embeddedResponses.add(new Constants.EmbeddedResponse("https://exp.host/@[.....YOUR PROFILE....]/[.....YOUR SLUG....]", "assets://shell-app-manifest.json", "application/json"));
             embeddedResponses.add(new Constants.EmbeddedResponse("[....URL FROM `dist/android-index.json` #bundleUrl.....]", "assets://shell-app.bundle", "application/json"));
         in `./android/app/expo.gradle` replace these two lines with `commandLine("ls")`... (or you can automate the moving of the assets here)
             commandLine("cmd", "/c", ".\\node_modules\\expokit\\detach-scripts\\run-exp.bat", "bundle-assets", expoRoot, "--platform", "android", "--dest", assetsDir)
             commandLine("./node_modules/expokit/detach-scripts/run-exp.sh", "bundle-assets", expoRoot, "--platform", "android", "--dest", assetsDir)
                    Note that a commandLine is required because gradle is expected one.
        Switch build varriant to release
        edit app/build.gradle
                move signingConfigs above defaultConfig
                change signingConfigs.release
                    `System.getenv` is used to get it as an argument from command line. Remove this and just add them in as strings.
                        passwords
                        keyAlias
        run -> fix signing
            signing configs
                open store file and add passwords (make sure the step from above is done in conjunction with this. they need to be strings not `System.getenv`)
            default config
                set signing config to $signingConfigs.release
        (there is probably a better way to do the signing stuff. For now i'll leave this as is. We have automated IOS but not android yet.)
        test app in Studio without the expo packager running (no `expo start`)
        Distribute app like a normal app

I haven’t tried something like this myself, but wouldn’t the Building Standalone Apps on Your CI docs help with this?

Did you eject because you need to add some native code or ejecting is just side effect of your building process?

If it’s the first case and you need to be ejected you should use bare workflow. At this point you are not using any expo features, only unimodules (you are not using OTA, can’t build on our servers, client won’t work with ejected app)

If you don’t need to eject and you want to use expo client for development you can build apps using turtle-cli https://github.com/expo/turtle . To build unpublished app you can

  • disable OTA in app.json
  • expo export --public-url locahost:8000/dist - url to localhost
  • python3 -m http.server - or any other way to host it locally, server needs to be up only during build process
  • turtle-cli build:android --public-url locahost:8000/dist/android-index.json

We ejected because we did not want to give Expo our credentials, we did not want the facebook SDK bundled with our app or have to supply any advertising keys. We also did not want any of our code coming from expo servers at any time. We plan on eventually using notifications and over-the-air.

We love developing in expo compared to our old process. And find the expo app config process convenient for developing our apps that are branded and slightly changed for different customers but all have the same base code. This is why we stuck with expokit instead of removing all dependencies and going full native.

Our ideal workflow would be to develop in expo and arrive at xcode and android studio projects that we could then handle the publishing ourselves. (while still being able to run the app in xcode and android stuido)

To clarify some things that are never explicitly stated in the documentation.
When you run expo publish for a self hosted standalone app, will code/assets be delivered to the expo servers and ever supplied from them? Or does exporting change the publish process to point to our own server instead?
In the standalone apps from self hosted guide it still says you need to do a publish.
Is there a way to use turtle-cli to produce anything other than .apk and .ipa? There are config settings we change in app.json that are not picked up when we eject and have to change these manually in the plist (phone orientation settings).
If we can not use turtle-cli to create projects, will the expo bundle-assets command that is used in both the ejected android and ios projects point to our supplied url with expo export --public-url locahost:8000/dist so that we could use that instead of manually changing and moving the files?

We ejected because we did not want to give Expo our credentials, we did not want the facebook SDK bundled with our app or have to supply any advertising keys. We also did not want any of our code coming from expo servers at any time.

You can have all of these things in managed workflow except removing facebook sdk(or any other modules).

To clarify some things that are never explicitly stated in the documentation.
When you run expo publish for a self hosted standalone app, will code/assets be delivered to the expo servers and ever supplied from them? Or does exporting change the publish process to point to our own server instead?

export prepares bundle locally publish generates the same files and upload them on expo servers, important is how you build actual apk/ipa. If you are building(either on our server or in turtle-cli) with --public-url flag it will fetch bundle from that url.

Is there a way to use turtle-cli to produce anything other than .apk and .ipa?

not sure what do you mean? You can also create app bundle on android, is that what you asking?

If we can not use turtle-cli to create projects, will the expo bundle-assets command that is used in both the ejected android and ios projects point to our supplied url with expo export --public-url locahost:8000/dist so that we could use that instead of manually changing and moving the files?

For an ejected project, you would need to change urls in code, but I’m not 100% if this will be enough.

Using bare workflow should be much better option for you. Your js code stays exactly the same, you can use all of the unimodules, only unimodules explictly instaled are included in apk/ipa and you have regular react-native project.