├── .gitignore ├── reloader-client.js ├── common.js ├── package.js ├── LICENSE ├── CHANGELOG.md ├── README.md └── reloader-cordova.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.code-workspace 2 | node_modules 3 | npm-debug.log 4 | yarn-error.log 5 | .DS_STORE 6 | .vscode 7 | 8 | **/.idea/* 9 | !.idea/runConfigurations 10 | 11 | .versions 12 | -------------------------------------------------------------------------------- /reloader-client.js: -------------------------------------------------------------------------------- 1 | import { debugFn } from './common'; 2 | 3 | const Reloader = { 4 | initialize() { 5 | debugFn('no reloader on client, only on Cordova'); 6 | }, 7 | }; 8 | 9 | export { Reloader }; 10 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | import { getSettings } from 'meteor/quave:settings'; 2 | 3 | export const PACKAGE_NAME = 'quave:reloader'; 4 | export const settings = getSettings({ packageName: PACKAGE_NAME }); 5 | 6 | export const debugFn = (message, context) => { 7 | if (!settings.debug) { 8 | return; 9 | } 10 | // eslint-disable-next-line no-console 11 | console.log(`[${PACKAGE_NAME}] ${message}`, JSON.stringify(context)); 12 | }; 13 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'quave:reloader', 3 | version: '2.1.0', 4 | summary: 'More control over hot code push reloading', 5 | git: 'https://github.com/quavedev/reloader/', 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('2.16'); 10 | 11 | api.use( 12 | ['ecmascript', 'reload', 'reactive-var', 'tracker', 'launch-screen'], 13 | 'client' 14 | ); 15 | 16 | api.use('quave:settings@1.0.0'); 17 | 18 | api.mainModule('reloader-client.js', 'client'); 19 | api.mainModule('reloader-cordova.js', 'web.cordova'); 20 | }); 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jamie Loberman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.1.0 - 2024-09-13 4 | 5 | - Removes cordova-plugin-splashscreen 6 | 7 | ## 2.0.3 - 2022-02-10 8 | 9 | Prevent errors on resume if splashscreen is not defined. 10 | 11 | ## 2.0.2 - 2020-10-19 12 | ### New feature 13 | You can control from your app code when your app should update. 14 | 15 | ### Clean up 16 | We removed options that we don't believe are necessary, read more details [here](./README.md) 17 | 18 | ## 1.6.0 - 2020-06-12 19 | ### Config using Meteor.settings 20 | - Config now is set using `Meteor.settings.public.packages.reloader` object 21 | 22 | ## past (before quave fork) 23 | ## [1.5.0] - 2019-02-08 24 | ### Config using Meteor.settings 25 | - Config now is set using `Meteor.settings.public.reloader` object 26 | - Added more logs if `Meteor.settings.public.reloader.debug` is true 27 | 28 | ## [1.4.0] - 2018-10-03 29 | ### Independency from Blaze 30 | - Removed Blaze dependency 31 | - Removed underscore dependency 32 | 33 | ## [1.3.0] - 2017-11-13 34 | ### Updated 35 | 36 | - `cordova-plugin-splashscreen` from `4.0.0` to `4.1.0` 37 | 38 | ## [1.2.2] - 2016-10-05 39 | ### Fixed 40 | 41 | - Fix reloading when url contains a hash ([#10](https://github.com/jamielob/reloader/issues/10)) 42 | 43 | ## [1.2.0] - 2016-04-26 44 | ### Added 45 | - `launchScreenDelay` option 46 | - tests 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reloader 2 | 3 | More control over hot code push reloading for your mobile apps. A replacement 4 | for [`mdg:reload-on-resume`](https://github.com/meteor/mobile-packages/blob/master/packages/mdg:reload-on-resume/README.md) 5 | with more options and better UX. 6 | 7 | Before using this package we recommend that you understand what Hot Code Push is, you can learn all about it [here](https://guide.meteor.com/hot-code-push.html) 8 | 9 | We provide two ways for you to handle your app code updates: 10 | 11 | - Always reload 12 | - Reload when the app allows (recommended) 13 | 14 | ### Always reload 15 | 16 | You don't need to configure anything, your app is going to reload as soon as the 17 | code is received. 18 | 19 | If you want you can inform `launchScreenDelay` (0 by default) in milliseconds to 20 | hold your splashscreen longer, avoiding a flash when the app is starting and 21 | reloading. 22 | 23 | ```json 24 | "public": { 25 | "packages": { 26 | "quave:reloader": { 27 | "launchScreenDelay": 200 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | ### Reload when the app allows 34 | 35 | We recommend this method as with it you can control when your app is going to 36 | reload. You can even delegate this decision to the final user. 37 | 38 | In this case you must use `automaticInitialization` as `false` in your settings. 39 | 40 | ```json 41 | "public": { 42 | "packages": { 43 | "quave:reloader": { 44 | "automaticInitialization": false 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | You also need to call 51 | `Reloader.initialize` in the render or initialization of your app providing a function (can be async) in the property `beforeReload`. 52 | 53 | ## Installing 54 | 55 | ```sh 56 | meteor add quave:reloader 57 | meteor remove mdg:reload-on-resume 58 | ``` 59 | 60 | ## Configuration Options 61 | 62 | ### idleCutoff 63 | 64 | Default: `1000 * 60 * 5 // 5 minutes` 65 | 66 | How long (in ms) can an app be idle before we consider it a start and not a 67 | resume. Applies only when `check: 'everyStart'`. Set to `0` to never check on 68 | resume. 69 | 70 | ### launchScreenDelay 71 | 72 | Default: `0` 73 | 74 | How long the splash screen will be visible. It's useful to avoid your app being rendered just for a few milliseconds and then refreshing. 75 | 76 | ### automaticInitialization 77 | 78 | Default: `true` 79 | 80 | If you want to initialize the `reloader` yourself you need to turn 81 | off `automaticInitialization`. This is useful when you want to provide code to 82 | some callback as this is not possible using JSON initialization. 83 | 84 | You can provide your callbacks calling Reloader.initialize(), for example: 85 | 86 | ```js 87 | ReloaderCordova.initialize({ 88 | beforeReload(ok, nok) { 89 | const isOkToReload = confirm('Your app will load now, ok?'); 90 | if (isOkToReload) { 91 | ok(); 92 | return; 93 | } 94 | nok(); 95 | }, 96 | }); 97 | ``` 98 | 99 | ## Example with React 100 | 101 | File: `Routes.js` (where we render the routes) 102 | ```javascript 103 | 104 | export const Routes = () => { 105 | useEffect(() => initializeReloader(), []); 106 | 107 | return ( 108 | 109 | // React router routes... 110 | 111 | ); 112 | } 113 | ``` 114 | 115 | File: `initializeReloader.js` 116 | ```javascript 117 | import { Reloader } from 'meteor/quave:reloader'; 118 | import { loggerClient } from 'meteor/quave:logs/loggerClient'; 119 | import { showConfirm } from './ConfirmationDialog'; 120 | import { methodCall } from '../../methods/methodCall'; 121 | import { version } from '../../version'; 122 | 123 | export const initializeReloader = () => { 124 | loggerClient.info({ message: 'initializeReloader' }); 125 | Reloader.initialize({ 126 | async beforeReload(updateApp, holdAppUpdate) { 127 | loggerClient.info({ message: 'initializeReloader beforeReload' }); 128 | let appUpdateData = {}; 129 | try { 130 | appUpdateData = 131 | (await methodCall('getAppUpdateData', { clientVersion: version })) || 132 | {}; 133 | } catch (e) { 134 | loggerClient.info({ 135 | message: 'forcing app reload because getAppUpdateData is breaking', 136 | }); 137 | updateApp(); 138 | return; 139 | } 140 | loggerClient.info({ 141 | message: 'initializeReloader beforeReload appUpdateData', 142 | appUpdateData, 143 | }); 144 | if (appUpdateData.ignore) { 145 | loggerClient.info({ 146 | message: 147 | 'initializeReloader beforeReload appUpdateData ignore is true', 148 | appUpdateData, 149 | }); 150 | return; 151 | } 152 | const cancelAction = appUpdateData.forceUpdate 153 | ? updateApp 154 | : holdAppUpdate; 155 | try { 156 | const message = appUpdateData.forceUpdate 157 | ? 'Precisamos atualizar o aplicativo. É rapidinho!' 158 | : 'Deseja atualizar agora? É rapidinho!'; 159 | const result = await showConfirm({ 160 | autoFocus: false, 161 | title: appUpdateData.title || 'Atualização disponível', 162 | content: appUpdateData.message || message, 163 | confirmText: appUpdateData.actionLabel || 'Beleza', 164 | cancelText: appUpdateData.noActionLabel || 'Mais tarde', 165 | hideCancel: !!appUpdateData.forceUpdate, 166 | dismiss: cancelAction, 167 | onCancel() { 168 | loggerClient.info({ 169 | message: 'initializeReloader beforeReload onCancel', 170 | appUpdateData, 171 | }); 172 | cancelAction(); 173 | }, 174 | }); 175 | loggerClient.info({ 176 | message: `initializeReloader beforeReload showConfirm result is ${result}`, 177 | appUpdateData, 178 | }); 179 | if (result) { 180 | loggerClient.info({ 181 | message: 'initializeReloader beforeReload showConfirm ok', 182 | appUpdateData, 183 | }); 184 | updateApp(); 185 | return; 186 | } 187 | loggerClient.info({ 188 | message: 'initializeReloader beforeReload showConfirm nok', 189 | appUpdateData, 190 | }); 191 | cancelAction(); 192 | } catch (e) { 193 | loggerClient.info({ 194 | message: 'initializeReloader beforeReload showConfirm catch call nok', 195 | appUpdateData, 196 | }); 197 | cancelAction(); 198 | } 199 | }, 200 | }); 201 | }; 202 | 203 | ``` 204 | 205 | File: `getAppUpdateData.js` 206 | ```javascript 207 | import { Meteor } from 'meteor/meteor'; 208 | import { logger } from 'meteor/quave:logs/logger'; 209 | import { AppUpdatesCollection } from '../db/AppUpdatesCollection'; 210 | import { version } from '../version'; 211 | 212 | Meteor.methods({ 213 | getAppUpdateData({ clientVersion } = {}) { 214 | this.unblock(); 215 | 216 | if (Meteor.isClient) return null; 217 | 218 | const appUpdate = AppUpdatesCollection.findOne() || {}; 219 | 220 | const result = { 221 | ...appUpdate, 222 | ...(appUpdate.ignoreVersions && 223 | appUpdate.ignoreVersions.length && 224 | appUpdate.ignoreVersions.includes(version) 225 | ? { ignore: true } 226 | : {}), 227 | version, 228 | }; 229 | logger.info({ 230 | message: `getAppUpdateData clientVersion=${clientVersion}, newClientVersion=${version}, ${JSON.stringify( 231 | result 232 | )}`, 233 | appUpdateData: appUpdate, 234 | appUpdateResult: result, 235 | clientVersion, 236 | }); 237 | return result; 238 | }, 239 | }); 240 | 241 | ``` 242 | -------------------------------------------------------------------------------- /reloader-cordova.js: -------------------------------------------------------------------------------- 1 | import {Meteor} from 'meteor/meteor'; 2 | import {Tracker} from 'meteor/tracker'; 3 | import {ReactiveVar} from 'meteor/reactive-var'; 4 | import {LaunchScreen} from 'meteor/launch-screen'; 5 | import {settings, debugFn, PACKAGE_NAME} from './common'; 6 | 7 | const DEFAULT_OPTIONS = { 8 | idleCutoff: 1000 * 60 * 5, // 5 minutes 9 | automaticInitialization: true, 10 | launchScreenDelay: 0, 11 | }; 12 | 13 | debugFn('starting - DEFAULT_OPTIONS', {DEFAULT_OPTIONS}); 14 | debugFn('starting - settings', {settings}); 15 | const options = Object.assign({}, DEFAULT_OPTIONS, settings); 16 | 17 | debugFn('starting - options', {options}); 18 | 19 | const defaultRetry = () => console.log(`[${PACKAGE_NAME}] no retry function yet`); 20 | 21 | const launchScreen = LaunchScreen.hold(); 22 | 23 | let initialized = false; 24 | 25 | const Reloader = { 26 | _options: {}, 27 | // eslint-disable-next-line no-console 28 | _retry: defaultRetry, 29 | updateAvailable: new ReactiveVar(false), 30 | isChecked: new ReactiveVar(false), 31 | 32 | debug(message, context) { 33 | debugFn(message, { 34 | ...context, 35 | updateAvailable: this.updateAvailable.get(), 36 | isChecked: this.isChecked.get(), 37 | _options: this._options, 38 | }); 39 | }, 40 | 41 | initialize(optionsParam = {}) { 42 | if (initialized) { 43 | return; 44 | } 45 | initialized = true; 46 | this._options = Object.assign({}, options, optionsParam); 47 | this._onPageLoad(); 48 | }, 49 | 50 | prepareToReload() { 51 | this.debug('prereload - show splashscreen'); 52 | // Show the splashscreen 53 | navigator.splashscreen.show(); 54 | 55 | const currentDate = Date.now(); 56 | this.debug('prereload - reloaderWasRefreshed', {currentDate}); 57 | // Set the refresh flag 58 | localStorage.setItem('reloaderWasRefreshed', currentDate); 59 | }, 60 | 61 | reloadNow() { 62 | this.debug('reloadNow'); 63 | if (this._isCheckBeforeReload() && !this.isChecked.get()) { 64 | this.debug( 65 | 'not reloading because beforeReload is provided and it is not checked yet' 66 | ); 67 | return; 68 | } 69 | this.prepareToReload(); 70 | 71 | // We'd like to make the browser reload the page using location.replace() 72 | // instead of location.reload(), because this avoids validating assets 73 | // with the server if we still have a valid cached copy. This doesn't work 74 | // when the location contains a hash however, because that wouldn't reload 75 | // the page and just scroll to the hash location instead. 76 | if (window.location.hash || window.location.href.endsWith('#')) { 77 | this.debug('reloadNow - reload'); 78 | window.location.reload(); 79 | } else { 80 | this.debug('reloadNow - replace'); 81 | window.location.replace(window.location.href); 82 | } 83 | }, 84 | 85 | // Should check if a cold start and (either everyStart is set OR firstStart 86 | // is set and it's our first start) 87 | _shouldCheckForUpdateOnStart() { 88 | this.debug('_shouldCheckForUpdateOnStart'); 89 | 90 | const isColdStart = !localStorage.getItem('reloaderWasRefreshed'); 91 | const reloaderLastStart = localStorage.getItem('reloaderLastStart'); 92 | this.debug('_shouldCheckForUpdateOnStart - info', { 93 | isColdStart, 94 | check: this._options.check, 95 | reloaderLastStart, 96 | }); 97 | const should = 98 | isColdStart; 99 | 100 | this.debug('_shouldCheckForUpdateOnStart - should', {should}); 101 | return should; 102 | }, 103 | 104 | // Check if the idleCutoff is set AND we exceeded the idleCutOff limit AND the everyStart check is set 105 | _shouldCheckForUpdateOnResume() { 106 | this.debug('_shouldCheckForUpdateOnResume'); 107 | const reloaderLastPause = localStorage.getItem('reloaderLastPause'); 108 | // In case a pause event was missed, assume it didn't make the cutoff 109 | if (!reloaderLastPause) { 110 | this.debug('_shouldCheckForUpdateOnResume no reloaderLastPause'); 111 | return false; 112 | } 113 | 114 | // Grab the last time we paused 115 | const lastPause = Number(reloaderLastPause); 116 | 117 | // Calculate the cutoff timestamp 118 | const idleCutoffAt = Number(Date.now() - this._options.idleCutoff); 119 | 120 | this.debug('_shouldCheckForUpdateOnResume - info', { 121 | idleCutoff: this._options.idleCutoff, 122 | check: this._options.check, 123 | lastPause, 124 | idleCutoffAt, 125 | }); 126 | return ( 127 | this._options.idleCutoff && 128 | lastPause < idleCutoffAt 129 | ); 130 | }, 131 | 132 | _waitForUpdate(computation) { 133 | this.debug('_waitForUpdate'); 134 | // Check if we have a HCP after the check timer is up 135 | Meteor.setTimeout(() => { 136 | // If there is a new version available 137 | if (this.updateAvailable.get()) { 138 | this.debug('_waitForUpdate - reloadNow'); 139 | this.reloadNow(); 140 | } else { 141 | // Stop waiting for update 142 | if (computation) { 143 | computation.stop(); 144 | } 145 | 146 | this.debug('prereload - release launchScreen'); 147 | launchScreen.release(); 148 | 149 | if ( 150 | !navigator || 151 | !navigator.splashscreen || 152 | !navigator.splashscreen.hide 153 | ) { 154 | console.warn( 155 | `[${PACKAGE_NAME}] navigator.splashscreen.hide not available` 156 | ); 157 | return; 158 | } 159 | this.debug('prereload - hide splashscreen'); 160 | navigator.splashscreen.hide(); 161 | } 162 | }, 0); 163 | }, 164 | 165 | _checkForUpdate() { 166 | this.debug('_checkForUpdate'); 167 | if (this.updateAvailable.get()) { 168 | // Check for an even newer update 169 | this.debug('_checkForUpdate - check for an even newer update'); 170 | this._waitForUpdate(); 171 | } else { 172 | // Wait until update is available, or give up on timeout 173 | Tracker.autorun(c => { 174 | if (this.updateAvailable.get()) { 175 | this.debug('_checkForUpdate - reloadNow'); 176 | this.reloadNow(); 177 | } 178 | 179 | this._waitForUpdate(c); 180 | }); 181 | } 182 | }, 183 | 184 | _onPageLoad() { 185 | this.debug('_onPageLoad'); 186 | if (this._shouldCheckForUpdateOnStart()) { 187 | this._checkForUpdate(); 188 | } else { 189 | Meteor.setTimeout(() => { 190 | this.debug('_onPageLoad - release launchScreen'); 191 | launchScreen.release(); 192 | 193 | // Reset the reloaderWasRefreshed flag 194 | localStorage.removeItem('reloaderWasRefreshed'); 195 | }, this._options.launchScreenDelay); // Short delay helps with white flash 196 | } 197 | }, 198 | 199 | _onResume() { 200 | this.debug('_onResume'); 201 | const shouldCheck = this._shouldCheckForUpdateOnResume(); 202 | 203 | localStorage.removeItem('reloaderLastPause'); 204 | 205 | if (shouldCheck) { 206 | // Show the splashscreen if available (this has been causing issues on iOS) 207 | if (navigator && navigator.splashscreen && navigator.splashscreen.show) { 208 | this.debug('_onResume - show splashscreen'); 209 | navigator.splashscreen.show(); 210 | } 211 | 212 | this._checkForUpdate(); 213 | return; 214 | } 215 | }, 216 | 217 | _isCheckBeforeReload() { 218 | this.debug('_isCheckBeforeReload'); 219 | return !!this._options.beforeReload && typeof this._options.beforeReload === 'function'; 220 | }, 221 | 222 | _callBeforeLoad() { 223 | this.debug('_callBeforeLoad'); 224 | const updateApp = () => { 225 | this.debug('_callBeforeLoad set isChecked to true'); 226 | this.isChecked.set(true); 227 | this._retry(); 228 | }; 229 | const holdAppUpdate = () => { 230 | this.debug('_callBeforeLoad set isChecked to false'); 231 | this.isChecked.set(false); 232 | }; 233 | this._options.beforeReload(updateApp, holdAppUpdate); 234 | }, 235 | 236 | // https://github.com/meteor/meteor/blob/devel/packages/reload/reload.js#L104-L122 237 | _onMigrate(retry) { 238 | this.debug('_onMigrate'); 239 | this._retry = retry || this._retry; 240 | if (!this._isCheckBeforeReload() || this.isChecked.get()) { 241 | // we are calling prepareToReload because as we are returning true the reload 242 | // will happen in the reload package then we are updating our timestamps 243 | this.prepareToReload(); 244 | 245 | this.isChecked.set(false); 246 | return [true, {}]; 247 | } 248 | 249 | if (this._isCheckBeforeReload()) { 250 | this._callBeforeLoad(); 251 | } 252 | 253 | // Set the flag 254 | this.updateAvailable.set(true); 255 | 256 | // Don't refresh yet 257 | return [false]; 258 | }, 259 | }; 260 | 261 | if (options.automaticInitialization) { 262 | Reloader.initialize(); 263 | } 264 | 265 | // Set the last start flag 266 | localStorage.setItem('reloaderLastStart', Date.now()); 267 | 268 | // Watch for the app resuming 269 | document.addEventListener( 270 | 'resume', 271 | () => { 272 | Reloader._onResume(); 273 | }, 274 | false 275 | ); 276 | 277 | localStorage.removeItem('reloaderLastPause'); 278 | 279 | // Watch for the device pausing 280 | document.addEventListener( 281 | 'pause', 282 | () => { 283 | // Save to localStorage 284 | localStorage.setItem('reloaderLastPause', Date.now()); 285 | }, 286 | false 287 | ); 288 | 289 | // Capture the reload 290 | // import { Reload } from 'meteor/reload' is not working 291 | // eslint-disable-next-line no-undef 292 | Reload._onMigrate(`${PACKAGE_NAME}`, retry => Reloader._onMigrate(retry)); 293 | 294 | export {Reloader}; 295 | --------------------------------------------------------------------------------