mgscreativa / react-native-code-push-expo-plugin-managed-workflow

react-native-code-push-expo-plugin-managed-workflow
6 stars 1 forks source link

[Question] Codepush Integration #1

Closed mvolonnino closed 10 months ago

mvolonnino commented 10 months ago

@mgscreativa - Hey man! been digging this past week trying to get codepush to work within my Expo managed project.

I have codepush setup as well through app center, and can also create and deploy codepush bundles to appcenter as well.

The steps i use to create a codepush update & test it:

package.json scripts:

"update:staging:ios": "appcenter codepush release-react -a org/App -d Staging -e index.tsx --plist-file ios/App/Info.plist",
"update:staging:android": "appcenter codepush release-react -a org/App-1 -d Staging -e index.tsx",
 "update:staging:all": "yarn run update:staging:ios && yarn run update:staging:android",

Steps in order:

  1. two new eas builds under the preview-staging profile for both ios and android
  2. npx expo prebuild to generate the ios and android folders needed locally for the codepush update
  3. yarn update:staging:all to which i get successfull codepush bundles and releases to appcenter

Android is not working at all for me when it comes to codepush updates - when trying through a manual approach, it would see that there is an update for the installed version, download it, then when trying to install it i would run into this error:

codepush error update is invalid - a JS bundle file name "null"

When it comes to iOS on both the manual approach & the silent approach through only the codepush decorator, it would download, install, and restart the app. After it restarted it would succesfully use the codepush bundle, but upon force closing the app and then re opening, it would not use the bundle anymore and be on the original js bundle included in the adhoc release.

Im using Expo sdk 49 with Expo Router with the expo-dev-client setup.

I have internal adhoc testing setup as well where I can utilize the preview-staging build profiles that has also been working as expected, I have been able to continuously utilize eas build commands to build new builds for testing through preview-staging with everything working.

My current setup in eas.json.

{
  "cli": {
    "version": ">= 3.13.2"
  },
  "build": {
    // * Base build profiles that can be extended
    // this will run a dev staging build, and have QR code to scan for device
    "dev-staging": {
      "distribution": "internal",
      "developmentClient": true,
      "channel": "dev-staging", // this is used for EAS Updates to target specific builds
      "env": {
        "APP_ENV": "STAGING",
        "ANDROID_GOOGLE_SERVICES": "./google-services/google-services.json",
        "IOS_GOOGLE_SERVICES": "./google-services/GoogleService-Info.plist"
      }
    },
    // this will run a dev production build, and have QR code to scan for device
    "dev-production": {
      "distribution": "internal",
      "developmentClient": true,
      "channel": "dev-production",
      "env": {
        "APP_ENV": "PRODUCTION",
        "ANDROID_GOOGLE_SERVICES": "./google-services/google-services.json",
        "IOS_GOOGLE_SERVICES": "./google-services/GoogleService-Info.plist"
      }
    },
    // ...

    // * Build profiles that extend the base build profiles for simulator
    // this will run a dev simulator build, and can download & install on simulator through the CLI
    "dev-simulator": {
      "extends": "dev-staging",
      "channel": "dev-simulator",
      "ios": {
        "simulator": true
      }
    },
    "prod-simulator": {
      "extends": "dev-production",
      "ios": {
        "simulator": true
      }
    },
    // ...

    // * Build profiles used for preview builds (adhoc internal testing)
    // channels can be used for EAS Updates to target specific builds
    "preview-staging": {
      "extends": "dev-staging",
      "channel": "preview-staging",
      "developmentClient": false
    },
    "preview-production": {
      "extends": "dev-production",
      "channel": "preview-production",
      "developmentClient": false
    },
    // ...

    // * Build profiles used for release builds (app stores & test flight)
    // channels can be used for EAS Updates to target specific builds
    "release-beta": {
      "extends": "preview-staging",
      "channel": "release-beta",
      "distribution": "store"
    },
    "release": {
      "extends": "preview-production",
      "channel": "release",
      "distribution": "store"
    }
  },
  "submit": {
    "release-beta": {
      "android": {
        "track": "internal",
        "releaseStatus": "draft"
      }
    },
    "release": {
      "android": {
        "track": "production",
        "releaseStatus": "draft"
      }
    }
  }
}

I have an index.tsx file right now that looks like this:

import '@expo/metro-runtime';

import { registerRootComponent } from 'expo';
import { ExpoRoot } from 'expo-router';

import deepLinkHandlers from 'features/DeepLinking';
import { nativeSentryWrap, sentryInit } from 'sentry/config';
import codePushWrap from 'updates/config';

/*
  Here we setup all notification handlers that are needed outside the scope of React
  - this are handlers that mostly handle notifications when the app is in background/quit state
  - very inconsistent on iOS and hard to debug
*/
deepLinkHandlers.backgroundHandlers.setBackgroundMessageHandler();

sentryInit();
/**
 * updated entry point to be able to utilize `sentry` and fix an issue with `expo-router`
 *
 * @see https://docs.expo.dev/router/reference/troubleshooting/
 */
export const App = () => {
  const ctx = require.context('./app');
  return <ExpoRoot context={ctx} />;
};

const CodePushedApp = codePushWrap(nativeSentryWrap(App));
registerRootComponent(CodePushedApp);

codePushWrap is just a simple wrapper method that uses codePush

/**

/**

export default codePushWrap;


My manual approach looks like this:
- this handles each step so i can show UI gracefully to the user
- This manual approach worked for iOS with the codePush.restartApp. But upon user closing app, it reverted and didnt use the codepush bundle (on the codepush website, it shows 1 install, 1 download, 0 rollbacks as well)
- Android is where i got that error above.

/ eslint-disable @typescript-eslint/no-use-before-define / import { Alert, Platform } from 'react-native'; import { RemotePackage } from 'react-native-code-push';

import { useSettingsStore } from 'stores'; import Toast from 'layouts/ToastLayout/Toast'; import Wait from 'utils/Wait'; import codePush from './codePush';

const { updateAppStateSlice } = useSettingsStore.getState();

export enum UpdateCheck { Interactive = 'interactive', Silent = 'silent', }

async function checkForUpdates(type: UpdateCheck) { /**

export default checkForUpdates;

/**

/**

/**

/**

/**

mvolonnino commented 10 months ago

@mgscreativa - i know this is a lot of information but wasnt sure if you had any insights for me or not, as I see your example utilizes expo and expo router too!

Banging my head against a wall right now trying to figure it out, so anything could be helpful 🙌

mvolonnino commented 10 months ago

@mgscreativa - I should note, after iOS silent codepush successful update and app suspend that did apply the codepush bundle - then after force closing the app to see if it would still be applied and wasnt, when I try my manual approach (through a button in a drawer component), it hits the no update available showing to me that the app does have the update downloaded already?

mgscreativa commented 10 months ago

Hi! that's really strange because upon update, the bundle gets overwritten. The best approach I suggest is that you download and compile test apps with my project and try to replicate the issue, If you can't then there's an issue with your code.

Another thing I notice is that if you see ios an android folders in your project, then it's not an expo managed project. This example is an Expo managed app that uses a dev build to work

Another thing I notice is in your codepush build command, I think you missed --use-hermes and --target-binary-version, please check https://github.com/mgscreativa/react-native-code-push-expo-plugin-managed-workflow#send-a-bundle-update-release

mvolonnino commented 10 months ago

@mgscreativa - so with the prebuild command I am doing and the --plist-file ios/App/Info.plist attached to the script, it grabs the correct target binary automatically and creates the bundles pointed towards what is in the app.json.

I will definitely try adding the --use-hermes as well.

Another thing I notice is that if you see ios an android folders in your project, then it's not an expo managed project. This example is an Expo managed app that uses a dev build to work

In terms of this, how do you go about creating a build that does not have the devClient: true, as if that is the case, to my knowledge - codepush bundle is not used and instead the dev clients bundle is always used with hot reload?

Currently I am using eas build --profile preview-staging --platform all which creates a build that does not utilize the devClient to be able to then test a codepush bundle on a 'release' mode of the app (with android its just the apk i download on my android device which is the standalone app, and iOS utilizing the adhoc internal distro for my own device to also get the standalone app to test as well.

Am I off basis here in my thinking?

mvolonnino commented 10 months ago

I guess a better way to state this, I would not really need codepush OTA on dev client builds as that is where I currently do development, I would need them for the production/beta builds that are distributed through the app stores & TestFlight (for my QA testers) - which in my thought process would be the same as a standalone app which is what my preview- profiles in the eas.json creates for me.

mvolonnino commented 10 months ago

Okay so adding the --use-hermes does not help either, still on iOS, it does download and install correctly to which after that first reload, shows the codepush bundle, but then after the next restart, just reverts back to the original bundle

mvolonnino commented 10 months ago

Upon further testing, the bundle does correctly install like ive said, I have methods setup to attach on the codepush label in the version im running to be able to tell which codepush update the app is running. That all works just fine on iOS, once I close out the app, the I have this method to run:

import { useEffect } from 'react';
import { Alert, AppState } from 'react-native';
import moment from 'moment-timezone';

import { useSettingsStore } from 'stores';
import codePush from 'updates/codePush';
import checkForUpdates, { UpdateCheck } from '../checkForUpdates';

const { updateAppStateSlice } = useSettingsStore.getState();

/**
 * Checks to see if the last check time is more than an hour ago
 * - rate limit of 8 request per 5 mins
 * @see https://learn.microsoft.com/en-us/appcenter/distribution/codepush/
 */
const hasEnoughHoursPast = (lastCheckTime: number | null, numHr: number) => {
  if (!lastCheckTime) return true;
  return moment().diff(moment(lastCheckTime), 'hours') >= numHr;
};

/**
 * Hook that checks for updates on app state change
 * - this is used to check for updates when the app is in `active` state
 * - defaults to `UpdateCheck.Silent` which should only show an indicator that an update is available to not interrupt the user (we do not want to interrupt the user if they are in the middle of something, i.e registering for a program, etc)
 *
 * @default UpdateCheck.Silent
 */
const useCheckForUpdates = (type: UpdateCheck = UpdateCheck.Silent) => {
  useEffect(() => {
    const listener = AppState.addEventListener('change', nextAppState => {
      const { lastSilentUpdateCheckTime } = useSettingsStore.getState().appState;

      const shouldCheckForUpdates =
        nextAppState === 'active' && hasEnoughHoursPast(lastSilentUpdateCheckTime, 1);

      if (shouldCheckForUpdates) {
        console.log(`[useCheckForUpdates][CodePush] Checking for updates`);
        (async () => {
          await checkForUpdates(type);
          updateAppStateSlice({ lastSilentUpdateCheckTime: moment().valueOf() });
        })();
      }
    });

    return () => {
      listener.remove();
    };
  }, [type]);

  /**
   * This checks to see if the app is now running on a new version that was just installed from codepush
   * - will update the `codePushLabel` in the app state to reflect the new version, and clear the `update` from the app state since we are now running on the latest update from codepush or version from the app store
   */
  useEffect(() => {
    (async () => {
      const LocalPackage = await codePush.getUpdateMetadata();
      Alert.alert('LocalPackage', JSON.stringify(LocalPackage));

      if (LocalPackage?.isFirstRun) {
        console.log(
          `[useCheckForUpdates][CodePush] Update Metadata: ${JSON.stringify(LocalPackage)}`
        );
        const { label } = LocalPackage;
        updateAppStateSlice({ codePushLabel: label });
      } else if (!LocalPackage) {
        console.log(
          `[useCheckForUpdates][CodePush] No update metadata found, running on latest released version`
        );
        updateAppStateSlice({ codePushLabel: null });
      }
    })();
  }, []);
};

export default useCheckForUpdates;

And the LocalPackage within the second useEffect does show in the Alert.alert, which shows some keys points that all look correct to me:

{
        isPending: false,
        packageSize: 2337248,
        appVersion: '2.2.9',
        packageHash: 'fakehash',
        downloadUrl: 'fakeurl',
        failedInstall: false,
        isFirstRun: false,
      }

But like I said, its not running the codepush bundle, its the original bundle

mgscreativa commented 10 months ago

I don't really know why that happens to you, I have it working in several production apps Expo managed, and locally compiled with EAS. This demo is based on this PR https://github.com/microsoft/react-native-code-push/pull/2415 from @deggertsen, you can take a look at the PR and update my test project files just to check.

Another check you can do is to use this repo and compile it and send it to test flight to see if the modified bundle hits the app after restart. If it does, then there's an issue with your project.

mvolonnino commented 10 months ago

Damn that is wild, I wonder what are your steps for building a standalone app? From your commands it looks like you do it locally - have you ever tried using eas build for it to be built in their cloud workers?

Trying to not have to do local builds and need to involve xcode & android studio with the new workflow

mgscreativa commented 10 months ago

Local builds are not hard at all, you just need to setup the dev environment for iOS and android and EAS does the rest.

Didn't tried EAS cloud build, but believe me, local builds are the same and the error you account for is not related to the build process, because it will fail otherwise