Abort an Updates.fetchUpdateAsync() after a certain time [Expo/React native] - react-native

Expo React Native SDK Version: 46
Platforms: Android/iOS
Package concerned : Expo.Updates
Hello everyone, I want to programmatically check for new updates, without using the fallbackToCacheTimeout in app.json that will trigger the check of the new updates when the application is launched because like that I can't put a custom loading page.
So by doing this all by code as follow :
try{
const update = await Updates.checkForUpdateAsync();
if(update.isAvailable){
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
}else{}
}catch(err){}
But I want to be able to abort all those calls after a certain time (thus, the user that have a bad connection can use the app without waiting a very long time).
I check the documentation and I cannot found any method that allow this.
I dont't think it's possible to cancel a Promise for now in Javascript, or maybe any connection ?
Or does the "fallbackToCacheTimeout" value in the app.json will automatically apply to the fetch updates call of the Expo API?
Do someone have any idea how to do it ? :(

First of all I am assuming you have set updates.checkautomatically field to ON_ERROR_RECOVERY in app.json or app.config.js file. If not, please check the documentation. The reason why you need this is to avoid automatic updates which can also block your app on splash screen.
Updated Solution
Because of the limitation in javascript we can't cancel any external Promise (not created by us or when its reject method is not exposed to us). Also the function fetchUpdateAsync exposed to us is not a promise but rather contains fetch promise and returns its result.
So, here we have two options:
Cancel reloading the app to update after a timeout.
But note that updates will be fetched in background and stored on
the device. Next time whenever user restarts the app, update will
be installed. I think this is just fine as this approach doesn't
block anything for user and also there is a default timeout for http
request clients like fetch and axios so, request will error out in
case of poor/no internet connection.
Here is the code:
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
const updateFetchPromise = Updates.fetchUpdateAsync();
const timeoutInMillis = 10000; // 10 seconds
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject("timedout"), timeoutInMillis))
// This will return only one Promise
Promise.race([updateFetchPromise, timeoutPromise])
.then(() => Updates.reloadAsync())
.catch((error) => {
if (error === 'timedout') {
// Here you can show some toast as well
console.log("Updates were not cancelled but reload is stopped.")
} else if (error === 'someKnownError') {
// Handle error
} else {
// Log error and/or show a toast message
}
})
} else {
// Perform some action when update is not available
}
} catch (err) {
// Handle error
}
Change the expo-updates package just for your app using a patch
Here you can return a cancel method with Updates.fetchUpdateAsync() and use it with setTimeout to cancel the fetch request. I won't be providing any code for this part but if you are curious I can definitely provide some help.
Please refer this section to understand use of fallbackToCacheTimeout in eas updates.
Old solution:
Now, for aborting or bypassing the promise i.e. Updates.fetchUpdateAsync in your case. You can basically throw an Error in setTimeout after whatever time duration you want, so that, catch block will be executed, bypassing the promises.
Here is the old code :
try{
const update = await Updates.checkForUpdateAsync();
if(update.isAvailable){
// Throw error after 10 seconds.
const timeout = setTimeout(() => { throw Error("Unable to fetch updates. Skipping..") }, 10000)
await Updates.fetchUpdateAsync();
// Just cancel the above timeout so, no error is thrown.
clearTimeout(timeout)
await Updates.reloadAsync();
}else{}
}catch(err){}

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.

Testing Branch.IO referral links on react native

We use branch for referrals in our react native application. I have implemented branch successfully, now I want to test some scenarios.
When we click on a referral link, it navigates to app store or play store. But I want to do some debugging to identify if the params I pass are successfully send or not.
I have subscribed it like this. But how can I debug this with react-native debugger to see the console logs?
BranchIO.subscribe(async ({ error, params }) => {
if (error) {
console.log('Error from Branch: ', error);
return;
}
// Handle non-Branch URL.
if (params['+non_branch_link']) return;
// Indicates initialization success.
// No link was opened.
if (!params['+clicked_branch_link']) return;
const installParams = await BranchIO.getFirstReferringParams();
if (installParams?.$canonical_identifier === DeepLinkTypes.referral) {
store.dispatch(setReferralKey(installParams.referralKey));
}
// A Branch link was opened.
// Route link based on data in params
navigatePath(params.$deeplink_path);
});
To debug if you are able to fetch the associated link data or not you can put in a debugger at the following line-
const installParams = await BranchIO.getFirstReferringParams();
Also we would suggest you to kindly use/update the above line to use getLatestReferringParams
let lastParams = await branch.getLatestReferringParams()
instead of getFirstReferringParams() since there are some recent issues reported lately with this method.
You can print out the parameters in the console and see for the link data.

how to handle axios onError in nuxtjs plugin and show appropriate error message to user

I have a nuxt single page application. The api that I work with has a list of codes for various errors. So, in onError interceptor, the error has to be checked in a dictionary or in a more desired way in a json file. For this, I added a error-handler.js plugin in my nuxt project. But, I don't know how to read from json file.
1) Loading of the json file would occur every time an error thrown?
2) What is the best practice to show the error message to the user? Is this plugin only responsible to create the error-message and in the page try-catch is needed to toast that message?
export default function ({ $axios, store, app, redirect }) {
$axios.onError(error => {
if (error.config.hasOwnProperty('errorHandle') && error.config.errorHandle === false) {
return Promise.reject(error);
}
if (error.message === 'Network Error') {
error.message = 'check the Internet connection';
return
}
const code = parseInt(error.response && error.response.status)
if (error.response)
console.log('error.response', error.response.status, error.response)
if (error.response.data.Errors) {
let errMessage = ''
error.response.data.Errors.forEach(item => {
switch (item.Message.MessageText) {
case 'OrganizationNotFound':
errMessage = 'the organization that you are looking for does not exists'
break
}
})
}
}
}
In Nuxt, plugin code is loaded once or twice per user visit, after that in universal mode it is not executed (of course your onError handler will be). It's once if you make it a client/server-side only plugin, or twice if you need it both on client and server. In your case client-side only plugin sounds like a good choice - just make sure that loading of JSON goes outside of the onError function.
As for how to show it, it depends on your design. We are using Vuetify and have v-snackbar in default layout so it's on every page. Snackbar is bound to VueX value. Then your error plugin can populate that value as appropriate as it will have access to store. This keeps the "raising the error" (dispatch to store) and "showing the error" reasonably decoupled, whilst very reusable (components can also dispatch to store if they face a problem).

Service Worker - Wait for clients.openWindow to complete before postMessage

I am using service worker to handle background notifications. When I receive a message, I'm creating a new Notification using self.registration.showNotification(title, { icon, body }). I'm watching for the click event on the notification using self.addEventListener('notificationclick', ()=>{}). On click I'm checking to see if any WindowClient is open, if it is, I'm getting one of those window clients and calling postMessage on it to send the data from the notification to the app to allow the app to process the notification. Incase there is no open window I'm calling openWindow and once that completes I'm sending the data to that window using postMessage.
event.waitUntil(
clients.matchAll({ type: 'window' }).then((windows) => {
if (windows.length > 0) {
const window = windows[0];
window.postMessage(_data);
window.focus();
return;
}
return clients.openWindow(this.origin).then((window) => {
window.postMessage(_data);
return;
});
})
);
The issue I am facing is that the postMessage call inside the openWindow is never delivered. I'm guessing this is because the postMessage call on the WindowClient happens before the page has finished loading, so the eventListener is not registered to listen for that message yet? Is that right?
How do I open a new window from the service worker and postMessage to that new window.
I stumble this issue as well, using timeout is anti pattern and also might cause delay larger then the 10 seconds limit of chrome that could fail.
what I did was checking if I need to open a new client window.
If I didn't find any match in the clients array - which this is the bottle neck, you need to wait until the page is loaded, and this can take time and postMessage will just not work.
For that case I created in the service worker a simple global object that is being populated in that specific case for example:
const messages = {};
....
// we need to open new window
messages[randomId] = pushData.message; // save the message from the push notification
await clients.openWindow(urlToOpen + '#push=' + randomId);
....
In the page that is loaded, in my case React app, I wait that my component is mounted, then I run a function that check if the URL includes a '#push=XXX' hash, extracting the random ID, then messaging back to the service worker to send us the message.
...
if (self.location.hash.contains('#push=')) {
if ('serviceWorker' in navigator && 'Notification' in window && Notification.permission === 'granted') {
const randomId = self.locaiton.hash.split('=')[1];
const swInstance = await navigator.serviceWorker.ready;
if (swInstance) {
swInstance.active.postMessage({type: 'getPushMessage', id: randomId});
}
// TODO: change URL to be without the `#push=` hash ..
}
Then finally in the service worker we add a message event listener:
self.addEventListener('message', function handler(event) {
if (event.data.type === 'getPushMessage') {
if (event.data.id && messages[event.data.id]) {
// FINALLY post message will work since page is loaded
event.source.postMessage({
type: 'clipboard',
msg: messages[event.data.id],
});
delete messages[event.data.id];
}
}
});
messages our "global" is not persistent which is good, since we just need this when the service worker is "awaken" when a push notification arrives.
The presented code is pseudo code, to point is to explain the idea, which worked for me.
clients.openWindow(event.data.url).then(function(windowClient) {
// do something with the windowClient.
});
I encountered the same problem. My error was that I registered event handler on the window. But it should be registered on service worker like this:
// next line doesn't work
window.addEventListener("message", event => { /* handler */ });
// this one works
navigator.serviceWorker.addEventListener('message', event => { /* handler */ });
See examples at these pages:
https://developer.mozilla.org/en-US/docs/Web/API/Clients
https://developer.mozilla.org/en-US/docs/Web/API/Client/postMessage
UPD: to clarify, this code goes into the freshly opened window. Checked in Chromium v.66.

Redux—global error handler

I am running redux on node. To handle asynchronous actions, like reading a file or listing of a directory, I am using redux-thunk in combination with Promises. So a typical action can look like that:
const
fs = require('fs'),
{ promisify } = require('util'),
readdir = promisify(fs.readdir);
const listFiles = dir => dispatch =>
readdir(dir)
.then(files => dispatch({
type: '…',
payload: { files }
}));
So:
try {
store.dispatch(listFiles('/some/path'));
catch (error) {
//some rescue plan here,
//won't work if directory not exists
}
wont work here, because the action is asynchronous and right now, the only way I see to handle all errors is to add a .catch() to all promises in all actions and dispatch an error action there.
That has two downsides:
a lot of code repetition and
i need to know all possible errors in ahead.
So my question is: Is there any way to create a global error handler, which will also be called if an asynchronous action fails, such that I can add some error indicating information to the state, which can be displayed?
Could that be possible with a »storeEnhancer« or some »middleware«?
UPDATE
I could find something that is really helpful:
process.on('unhandledRejection', (reason, promise) => {
console.log(reason.message);
});
That callback is triggered whenever a Promise is rejected and no catch block is added. Right now that seams to do the trick, but anyway, I would prefer a solution that basically does the exact same thing, but only for rejected Promises which are triggered within store.dispatch(), so only when an error within the processing of actions/middleware/reducers within redux comes to happen.
If you're looking for a redux middleware solution, take a look at redux-catch.