microsoft / react-native-code-push

React Native module for CodePush
http://appcenter.ms
Other
8.99k stars 1.47k forks source link

[ios] Update lost after app restart #2089

Closed JinYuSha0 closed 3 years ago

JinYuSha0 commented 3 years ago

Thanks so much for filing an issue or feature request! Please fill out the following (wherever relevant):

Steps to Reproduce

1.launch app

  1. codePush update 3.restart app 4.update again

Part of code

RNServer.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNServer : NSObject

+(instancetype)codePush;
+(instancetype)localhost;
+(instancetype)url:(NSString *)url;
+(instancetype)bundle;

@property(nonatomic) NSURL *url;

@end

NS_ASSUME_NONNULL_END

RNServer.m

+(instancetype)codePush{
  RNServer *s = RNServer.new;
  NSString *path = [NSBundle.mainBundle pathForResource:@"rn" ofType:@"bundle"];
  if (path) {
    NSBundle *bundle = [NSBundle bundleWithPath:path];
    NSURL *url = [CodePush bundleURLForResource:@"main" withExtension:@"jsbundle" subdirectory:@"" bundle:bundle];
    s.url = url;
  } else {
    s.url = [NSURL URLWithString:@""];
  }
  return s;
}

RNBridge.m

#import <Foundation/Foundation.h>
#import <CodePush/CodePush.h>
#import "RNBridge.h"

@interface RNBridge() <RCTBridgeModule, RCTBridgeDelegate>
@end

@implementation RNBridge

#pragma RCTBridgeDelegate

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge{
  return RNBridge.server.url;
}

static RNServer *_server;
+ (RNServer *)server{
  if (!_server) {
    _server = [RNServer codePush];
  }
  return _server;
}

AppDelegate.swift

unc application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
  // ...
  RNBridge.server = RNServer.codePush()
  // ...
}

useCodePush.ts

import React, { useState, useEffect, useCallback, useRef } from 'react';
import CodePush, {
  RemotePackage,
  RollbackRetryOptions,
  SyncOptions,
  DownloadProgress,
  LocalPackage,
} from 'react-native-code-push';
import { IsIOS } from '@utils/constants';
import { useAppState } from '@hooks/useAppState';

interface PackageInfo {
  label: string;
  description: string;
  isMandatory: boolean;
  packageSize: number;
}

interface UpdateInfo {
  status: CodePush.SyncStatus;
  progressNumber: number;
}

const useCodePush = () => {
  const [packageInfo, setPackageInfo] = useState<PackageInfo | {}>({});
  const [updateInfo, setUpdateInfo] = useState<UpdateInfo | {}>({});
  const remotePackageRef = useRef<RemotePackage | null>(null);
  const localPackageRef = useRef<LocalPackage | null>(null);

  // 是否忽略此次更新
  const shouldUpdateBeIgnored = useCallback(
    (
      remotePackage: RemotePackage,
      syncOptions: SyncOptions & { ignoreFailedUpdates: boolean },
    ) => {
      return new Promise<boolean>((resolve, reject) => {
        let { rollbackRetryOptions } = syncOptions;
        const isFailedPackage = remotePackage && remotePackage.failedInstall;
        if (!isFailedPackage || !syncOptions.ignoreFailedUpdates) {
          return resolve(false);
        }

        if (!rollbackRetryOptions) {
          return resolve(true);
        }

        rollbackRetryOptions = {
          delayInHours: 24,
          maxRetryAttempts: 1,
        };

        try {
          const NativeCodePush = require('react-native').NativeModules.CodePush;
          NativeCodePush.getLatestRollbackInfo().then(
            (latestRollbackInfo: any) => {
              const {
                delayInHours,
                maxRetryAttempts,
              } = rollbackRetryOptions as RollbackRetryOptions;
              const hoursSinceLatestRollback =
                (Date.now() - latestRollbackInfo.time) / (1000 * 60 * 60);
              if (
                hoursSinceLatestRollback >= delayInHours! &&
                maxRetryAttempts! >= latestRollbackInfo.count
              ) {
                console.log(
                  'Previous rollback should be ignored due to rollback retry options.',
                );
                return resolve(false);
              }
            },
          );
        } catch (err) {
          return reject(err);
        }

        return resolve(true);
      });
    },
    [],
  );

  // 切换codepush状态
  const syncStatusChangeCallback = useCallback(
    (status: CodePush.SyncStatus) => {
      switch (status) {
        case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
        // 下载中
        case CodePush.SyncStatus.INSTALLING_UPDATE:
        // 正在安装最新版本
        case CodePush.SyncStatus.AWAITING_USER_ACTION:
        // 等待用户操作
        case CodePush.SyncStatus.UPDATE_INSTALLED:
          // 安装完成
          setUpdateInfo(oldState => ({ ...oldState, status: status }));
          break;
      }
    },
    [],
  );

  // codePush下载静态文件,处理下载进度
  const codePushDownLoad = useCallback((cb?: () => void) => {
    syncStatusChangeCallback(CodePush.SyncStatus.DOWNLOADING_PACKAGE);
    if (!remotePackageRef.current) return;
    remotePackageRef.current
      ?.download((progress: DownloadProgress) => {
        const { receivedBytes, totalBytes } = progress;
        const progressNumber = Math.ceil(
          Number(100 * (receivedBytes / totalBytes)),
        );
        if (progressNumber > 8) {
          setUpdateInfo(oldState => ({ ...oldState, progressNumber }));
        }
      })
      .then(localPackage => {
        syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
        localPackageRef.current = localPackage;
        cb && cb();
      });
  }, []);

  // codePush应用更新
  const codePushInstall = useCallback((installMode: CodePush.InstallMode) => {
    if (!localPackageRef.current) return;
    localPackageRef.current.install(installMode, 0);
    syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
  }, []);

  // codepush检查是否更新
  const codePushSync = useCallback(() => {
    return new Promise<CodePush.SyncStatus>((resolve, reject) => {
      const {
        status = CodePush.SyncStatus.UP_TO_DATE,
      } = updateInfo as UpdateInfo;
      if (status > CodePush.SyncStatus.UP_TO_DATE) return;
      const syncOptions = {
        checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME, // 每次恢复的时候检查更新
        installMode: CodePush.InstallMode.ON_NEXT_RESTART, // 下次启动的时候安装
        mandatoryInstallMode: CodePush.InstallMode.ON_NEXT_RESTART, // 强制更新也是在下次启动的时候才安装
      };
      CodePush.notifyAppReady().then(() => {
        syncStatusChangeCallback(CodePush.SyncStatus.CHECKING_FOR_UPDATE); // 切换到正在检查更新的状态
        CodePush.checkForUpdate().then(remotePackage => {
          shouldUpdateBeIgnored(remotePackage!, syncOptions as any).then(
            updateShouldBeIgnored => {
              if (!remotePackage || updateShouldBeIgnored) {
                if (updateShouldBeIgnored) {
                  console.log(
                    'An update is available, but it is being ignored due to having been previously rolled back.',
                  );
                }
                (CodePush as any).getCurrentPackage((currentPackage: any) => {
                  if (currentPackage && currentPackage.isPending) {
                    syncStatusChangeCallback(
                      CodePush.SyncStatus.UPDATE_INSTALLED,
                    );
                    return resolve(CodePush.SyncStatus.UPDATE_INSTALLED);
                  } else {
                    syncStatusChangeCallback(CodePush.SyncStatus.UP_TO_DATE);
                    return resolve(CodePush.SyncStatus.UPDATE_INSTALLED);
                  }
                });
              } else {
                const {
                  label,
                  isMandatory,
                  description,
                  packageSize,
                } = remotePackage;
                remotePackageRef.current = remotePackage;

                if (IsIOS) {
                  // ios静默更新
                  codePushDownLoad(
                    codePushInstall.bind(
                      null,
                      CodePush.InstallMode.ON_NEXT_SUSPEND,
                    ),
                  );
                } else {
                  // android非大陆地区需要弹框提示
                  setPackageInfo({
                    label,
                    isMandatory,
                    description,
                    packageSize,
                  });

                  syncStatusChangeCallback(
                    CodePush.SyncStatus.AWAITING_USER_ACTION,
                  );

                  // todo 显示弹框

                  return resolve(CodePush.SyncStatus.AWAITING_USER_ACTION);
                }
              }
            },
          );
        });
      });
    });
  }, []);

  useEffect(() => {
    codePushSync();
  }, []);

  // 每次唤醒app,检查更新
  useAppState('active', () => {
    codePushSync();
  });

  return {
    packageInfo,
    updateInfo,
    codePushInstall,
    codePushSync,
  };
};

export default useCodePush;

Environment

JinYuSha0 commented 3 years ago

I solved the problem, because i used --targetBinaryVersion param, and set it value "*"

 if ([[CodePushUpdateUtils modifiedDateStringOfFileAtURL:binaryBundleURL] isEqualToString:packageDate] && ([CodePush isUsingTestConfiguration] ||[binaryAppVersion isEqualToString:packageAppVersion])) {
        // Return package file because it is newer than the app store binary's JS bundle
        NSURL *packageUrl = [[NSURL alloc] initFileURLWithPath:packageFile];
        CPLog(logMessageFormat, packageUrl);
        isRunningBinaryVersion = NO;
        return packageUrl;
    } else {
        BOOL isRelease = NO;
#ifndef DEBUG
        isRelease = YES;
#endif

        if (isRelease || ![binaryAppVersion isEqualToString:packageAppVersion]) {
            [CodePush clearUpdates];
        }

        CPLog(logMessageFormat, binaryBundleURL);
        isRunningBinaryVersion = YES;
        return binaryBundleURL;
    }

It can lead to binaryAppVersion(1.0) is not equal packageAppVersion(*) and clearUpdates just set --targetBinaryVersion param value to current app version then solved

anhquan291 commented 1 year ago

Hi guys, I'm still facing the same issue only on iOS. I'm sure that I setup everything correctly since I use the same config for another project that is running on "react-native": "0.67.4" and it works perfectly.

"react-native": "0.68.2", "react-native-code-push": "^7.0.5"

appcenter codepush release-react -a ${APP_CENTER_ORGANIZATION}/${APP_CENTER_IDENTIFIER_IOS} -d ${APP_ENV_MODE} -t ${APP_VERSION} --token ${APP_CENTER_TOKEN}

  useEffect(() => {
    if (isLoadingComplete) {
      CodePush.sync({
        deploymentKey:
          Platform.OS === 'ios'
            ? appConfigs?.CODE_PUSH_KEY_IOS
            : appConfigs?.CODE_PUSH_KEY_ANDROID,
        updateDialog: false,
        installMode: CodePush.InstallMode.IMMEDIATE,
      });
    }
  }, [isLoadingComplete]);

Any helps guys? Really appreciate it

baixjian commented 1 year ago

code-push release-react my-app ios --des "bug-fix 2023/9/5 15:45:02-dev" --m true -d Production -t 1.0.11

still has problem