service-worker.js not receiving message - reactjs

Im trying to get my service-worker.js to execute skipWaiting when a new service worker gets uploaded. However, im not successful in performing this. Please note that my main framework is reactjs
Tried following this guide (https://deanhume.com/displaying-a-new-version-available-progressive-web-app/) and adapting the code to mine but wasnt successful either.
Currently Im only able to receive notifications that the a new service worker is available, this was observed and confirmed under the browser-dev-tools as well.
Here's my code:
index.js
import "#babel/polyfill";
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
function check() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js').then(reg => {
reg.addEventListener('updatefound', () => {
// A wild service worker has appeared in reg.installing!
const newWorker = reg.installing;
newWorker.addEventListener('statechange', () => {
// Has network.state changed?
switch (newWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// new update available
window.serviceWorkerCallback('new_version')
newWorker.postMessage({ action: 'skipWaiting' });
console.log("New Version!")
}
// No update available
break;
}
});
});
});
};
}
check();
window.serviceWorkerCallback('new_version') from index.js activates _serviceWorkerCallback function under App.js which triggers my reload command. After refreshing my page, I could see that the new service worker is still waiting. Below is my reload command code:
App.js:
_serviceWorkerCallback = (state) => {
if (state === 'new_version') {
this.setState({
hasNewVersion: true
});
}
}
_renderNewVersion = () => {
return (
<Snackbar
style={{ marginBottom: 16 }}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
open={this.state.hasNewVersion}
message={<span>A new version is available</span>}
action={<Button color="secondary" size="small" onClick={this._handleRefresh}>Refresh</Button>}
/>
);
}
_handleRefresh = (value) => {
this.setState({
hasNewVersion: false,
});
window.location.reload();
}
Here is service-worker.js :
self.addEventListener('message', event => {
console.log('Skipping');
if (event.data.action === 'skipWaiting') {
self.skipWaiting();
}
});
self.addEventListener('fetch', function (event) {
console.log('Fetching');
event.respondWith(
caches.match(event.request)
.then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
_handleRefresh = (value) => {
this.setState({
hasNewVersion: false,
});
window.location.reload();
}
Im expecting newWorker.postMessage({ action: 'skipWaiting' }); to trigger event.data.action === 'skipWaiting' but nothing happened and no log messages has been observed from the console.
Both files are residing under the same directory.

In the tutorial you linked, the code includes a call to reload the page but I do not see that in yours.
The reload is important because it allows the newly activated service worker to become the active worker once the page calls register. Alternatively you can use Clients.claim() to let your new service worker take immediate control of existing pages.

Related

How to create reusable chakra ui toast

I'm using chakra ui with my next js app.
On some event, I want to show a notification using chakra ui toast, after performing some action.
(For ex. on clicking sign in, I'll send request to backend, and will show success or error toast depending on the result)
And as this toast can only be invoked after click and not programatically, I've created a function to do so
import { useToast } from '#chakra-ui/react';
...
export default function SignInPage() {
const toast = useToast();
const resultToast = (status, title) => {
return toast({
position: "top",
title: title,
status: status,
duration: 3000,
isClosable: true,
});
const handleSubmit = (e) => {
// talking to backend / database
if(success) {
resultToast("success", "sign in successful");
}
else {
resultToast("error", "sign in failed");
}
}
// sign in form
};
}
This is working totally fine, but the thing is I want to use this on multiple pages, and want to make it reusable, but the issue is:
I can't make it a jsx component, as it returns only toast element and gets invoked only on click.
And I can't make it a normal function in separate file, as it uses useToast hook from chakra ui which can't be used in a function (Or maybe I'm wrong).
And also not able to export the resultToast function from one file, it shows "Modifiers can't appear here"
As I'm using chakra ui, so didn't want to install any other toast library. So is there any way to make this reusable or I'll have to use any external library or copy paste the function on all pages :D
I don't know if you ever figured this out, but I ran into the same problem today, and here's how I resolved it with Chakra-UI. Create a separate function and write the logic like this...
import { useToast } from "#chakra-ui/react";
export const CustomToast = () => {
const toast = useToast();
// types are: "success", "info", "warning", "error"
const addToast = (newRes) => {
toast({
description:newRes.message,
status: newRes.type,
position:"top-right",
isClosable: true,
duration: 5000,
variant: 'left-accent'
})
}
return { addToast };
}
Then import the addToast method anywhere in your application import { addToast } from "../path/to/file";, create an instance const { addToast } = CustomToast(); and use it by passing a destructured object to the addToast function addToast({message: "sign in successful", type: "success"})... I hope this helps someone else out here.
You can use react-universal-flash library to flash notification across nextjs pages .
Just add Flasher to _app.js
import { ChakraProvider} from "#chakra-ui/react";
import { Flasher } from "react-universal-flash";
import Message from "./Message";
function MyApp({ Component, pageProps }) {
return (
<ChakraProvider>
<Flasher>
<Message />
</Flasher>
<Component {...pageProps} />
</ChakraProvider>
);
}
Codesandbox sample of chakra-ui along with react-universal-flash is below
react-universal-flash + chakra-ui
here is my solution: I did make an object and used it:
const defaultToastProps = {
position: "top-right" as ToastPosition,
duration: 2000,
isClosable: true,
};
const submitHandler = () => {
Axios.post("/user/register", formData)
.then((res) => {
console.log(res);
toast({
title: "Account created.",
description: "We've created your account for you.",
status: "success",
...defaultToastProps,
});
nav("/login");
})
.catch((err) => {
if (err.response) {
console.log(err.response.data.message);
toast({
title: "Something went wrong.",
description: err.response.data.message,
status: "error",
...defaultToastProps,
});
} else {
console.log(err);
toast({
title: "Something went wrong.",
description: "server-error",
status: "error",
...defaultToastProps,
});
}
});
};

React - PayPal Button fires without checking conditions

I'm using react-paypal-express-checkout
I've to options: Cash and PayPal.
Cash working fine and checks all conditions.
But bcs PayPal is a seperate component in my CartScreen component it opens and don't check a single if conditions and opens the PayPal window
The CashButton comes with function "cashTranSuccess" it's the same function as "TranSuccess"
just without the paymentID bcs it's only needed for react-paypal-express-checkout
So what I'm looking for is, to check all TranSuccess() conditions before open the PayPal window.
PayPalButton.js
import React from 'react';
import PaypalExpressBtn from 'react-paypal-express-checkout';
export default class PayPalButton extends React.Component {
render() {
const onSuccess = (payment) => {
// Congratulation, it came here means everything's fine!
console.log('The payment was succeeded!', payment);
// You can bind the "payment" object's value to your state or props or whatever here, please see below for sample returned data
this.props.tranSuccess(payment);
};
const onCancel = (data) => {
// User pressed "cancel" or close Paypal's popup!
console.log('The payment was cancelled!', data);
// You can bind the "data" object's value to your state or props or whatever here, please see below for sample returned data
};
const onError = (err) => {
// The main Paypal's script cannot be loaded or somethings block the loading of that script!
console.log('Error!', err);
// Because the Paypal's main script is loaded asynchronously from "https://www.paypalobjects.com/api/checkout.js"
// => sometimes it may take about 0.5 second for everything to get set, or for the button to appear
};
let env = 'sandbox'; // you can set here to 'production' for production
let currency = 'EUR'; // or you can set this value from your props or state
let carttotal = this.props.carttotal; // same a s above, this is the total amount (based on currency) to be paid by using Paypal express checkout
// Document on Paypal's currency code: https://developer.paypal.com/docs/classic/api/currency_codes/
const client = {
sandbox:
'',
production: 'YOUR-PRODUCTION-APP-ID',
};
// In order to get production's app-ID, you will have to send your app to Paypal for approval first
// For sandbox app-ID (after logging into your developer account, please locate the "REST API apps" section, click "Create App"):
// => https://developer.paypal.com/docs/classic/lifecycle/sb_credentials/
// For production app-ID:
// => https://developer.paypal.com/docs/classic/lifecycle/goingLive/
// NB. You can also have many Paypal express checkout buttons on page, just pass in the correct amount and they will work!
// Style Options: https://developer.paypal.com/docs/checkout/standard/customize/buttons-style-guide/ ; https://wise.com/gb/blog/custom-paypal-button
let style = {
size: 'medium',
color: 'gold',
label: 'pay',
tagline: false,
};
return (
<PaypalExpressBtn
env={env}
client={client}
currency={currency}
total={carttotal}
onError={onError}
shipping={1}
onSuccess={onSuccess}
onCancel={onCancel}
style={style}
/>
);
}
}
CartScreen
const tranSuccess = async (payment) => {
const { paymentID } = payment;
// Check time, min amoint, for delivery add delivery fees
if (timeValidation === true) {
if (sliderDeliveryValue === 'delivery') {
if (carttotal > settings[0]?.minDeliveryAmount) {
await axios.post(
'/api/payment',
{ cartItems, paymentID, time, sliderDeliveryValue, carttotal },
{
headers: { Authorization: token },
}
);
cartItems.map((remove) => {
dispatch(deleteFromCart(remove));
});
//console.log(cartItems.length);
toast.success(
'Order successful',
{
position: toast.POSITION.TOP_RIGHT,
}
);
} else {
toast.error(
`Min amount${settings[0]?.minDeliveryAmount}€`,
{
position: toast.POSITION.TOP_RIGHT,
}
);
}
} else if (sliderDeliveryValue === 'pickup') {
if (carttotal > 2) {
await axios.post(
'/api/payment',
{ cartItems, paymentID, time, sliderDeliveryValue, carttotal },
{
headers: { Authorization: token },
}
);
cartItems.map((remove) => {
dispatch(deleteFromCart(remove));
});
//console.log(cartItems.length);
toast.success(
'Order successful',
{
position: toast.POSITION.TOP_RIGHT,
}
);
} else {
toast.error(`Min amount 2.00€`, {
position: toast.POSITION.TOP_RIGHT,
});
}
} else {
toast.error('Choose delivery method', {
position: toast.POSITION.TOP_RIGHT,
});
}
} else {
toast.error('closed', {
position: toast.POSITION.TOP_RIGHT,
});
}
};
<PayPalButton
carttotal={carttotal}
tranSuccess={tranSuccess}
/>
<div onClick={cashTranSuccess}>
<CashButton />
</div>
Consider using the official #paypal/react-paypal-js
An example of validation using onInit and onClick functions and the actions.enable/disable callbacks or returning a promise (actions.resolve/reject) can be found in the developer documentation. Adapt this to check whatever condition you need.

Cypress: testing React app in offline mode causes test to hang

I am testing a React application created using create-react-app. I overwrite the App.js file to display text depending on whether the browser is online/offline:
import { Offline, Online } from "react-detect-offline";
function App() {
return (
<div className="App">
<Online>
<p data-testid="online-text">online</p>
</Online>
<Offline>
<p data-testid="offline-text">offline</p>
</Offline>
</div>
);
}
export default App;
I then run npx cypress open and created the following test file:
/// <reference types="cypress" />
const goOffline = () => {
cy.log("**go offline**")
.then(() => {
Cypress.automation("remote:debugger:protocol", {
command: "Network.enable",
});
})
.then(() => {
Cypress.automation("remote:debugger:protocol", {
command: "Network.emulateNetworkConditions",
params: {
offline: true,
latency: -1,
downloadThroughput: -1,
uploadThroughput: -1,
},
});
});
};
const goOnline = () => {
cy.log("**go online**")
.then(() => {
Cypress.automation("remote:debugger:protocol", {
command: "Network.emulateNetworkConditions",
params: {
offline: false,
latency: -1,
downloadThroughput: -1,
uploadThroughput: -1,
},
});
})
.then(() => {
Cypress.automation("remote:debugger:protocol", {
command: "Network.disable",
});
});
};
describe("app", () => {
it("should render online text when online", () => {
goOnline();
cy.get("[data-testid='online-text']").should("exist");
goOnline();
});
it("should render offline text when offline", () => {
goOffline();
cy.get("[data-testid='offline-text']").should("exist");
goOnline();
});
it("should not render online text when offline", () => {
goOffline();
cy.get("[data-testid='online-text']").should("not.exist");
goOnline();
});
});
This tests the app in offline mode according to this guide. The first 2 tests run as expected but the 3rd test gets stuck in an infinite loop:
What Cypress version are you using?
I had a similar issue when after going Offline, Cypress couldn't go Online. After looking for an answer stumbled upon this reason https://github.com/cypress-io/cypress/issues/17723#issuecomment-906152925. Eventually, I was able to get my code working using Cypress version 6.9.1 and not 9.5.1

Ionic React: Implementing InAppPurchase 2 on React Hooks

I am trying to implement InAppPurchase 2 from https://github.com/j3k0/cordova-plugin-purchase in Ionic React.
It seems that the ready event is never been called.
Here is my code:
....
import { IAPProduct, InAppPurchase2 as iap } from "#ionic-native/in-app-purchase-2";
const App: React.FC = () => {
useEffect(() => {
const init = async () => {
await initInAppPurchase();
setInitialized(true);
}
init();
}, [])
....
}
export const initInAppPurchase = () => {
if (isPlatform('android') || isPlatform('ios')) {
iap.verbosity = iap.DEBUG;
iap.register({
id: "com.mysoftwares.posapp.test",
alias: "Test",
type: iap.NON_CONSUMABLE
});
iap.when("com.mysoftwares.posapp.test").updated((product: IAPProduct) => {
if (product.owned)
console.log('Product owned')
else
console.log('Product not owned')
});
iap.when("com.mysoftwares.posapp.test").approved(function (product: any) {
product.finish();
});
iap.ready(() => {
alert("Product ready!")
let product = iap.get('Test');
alert(product);
if (product.canPurchase) {
iap.order('Test');
}
})
iap.refresh();
}
}
Here is the log from debug verbose:
[store.js] DEBUG: state: com.mysoftwares.posapp.test -> registered
[store.js] DEBUG: store.trigger -> triggering action refreshed
InAppBilling[js]: setup ok
InAppBilling[js]: load ["com.mysoftwares.posapp.test"]
InAppBilling[js]: listener: {"type":"ready","data":{}}
[store.js] DEBUG: store.trigger -> triggering action refresh-finished
InAppBilling[js]: setup ok
InAppBilling[js]: load ["com.mysoftwares.posapp.test","com.mysoftwares.posapp.test"]
InAppBilling[js]: listener: {"type":"ready","data":{}}
InAppBilling[js]: setup ok
InAppBilling[js]: load ["com.mysoftwares.posapp.test","com.mysoftwares.posapp.test","com.mysoftwares.posapp.test"]
InAppBilling[js]: listener: {"type":"ready","data":{}}
InAppBilling[js]: setup ok
I am using React Hooks. I have published my app in Google Play Console and have added the test product in In-app Products section inside Google Play Console. Currently my app is on open testing phase.
I have created an app before coded in pure Apache Cordova without Ionic, it works fine, but this in React is not.
What is wrong with my code? Please help...
I am testing it on emulator which is dumb. On mobile devices, it works fine.
I had to do the following:
Install these in terminal
npm install #ionic-native/in-app-purchase-2
npm install cordova-plugin-purchase
This code in .tsx file
import React, { useState, useEffect } from 'react';
import { InAppPurchase2 as iap, IAPProduct } from "#ionic-native/in-app-purchase-2";
const Home: React.FC = () => {
//declare variables
const [productPrice, setPrice] = useState('')
const [product, setProduct] = useState([]) as any
//initiate initInAppPurchase function
useEffect(() => {
const init = async () => {
await initInAppPurchase();
}
init();
}, []);
//if on an ios or android device, then get product info
const initInAppPurchase = () => {
if ((isPlatform('ios')) || (isPlatform('android'))) {
iap.verbosity = iap.DEBUG;
iap.register({
id: "com.test.test",
alias: "Test",
type: iap.NON_CONSUMABLE
});
iap.ready(() => {
let product = iap.get('Test');
setPrice(product.price)
setProduct(product)
})
iap.refresh();
}
}
//if user clicks purchase button
const purchaseProduct = () => {
if (product.owned) {
alert('Product already owned, click restore button instead!')
} else {
iap.order('Test').then(() => {
iap.when("com.test.test").approved((p: IAPProduct) => {
//store product
p.verify();
p.finish();
});
})
iap.refresh();
}
}
//if user clicks retore or promo code button
const restore = () => {
iap.when("com.test.test").owned((p: IAPProduct) => {
if (product.owned) {
//store product
} else {
alert("You have not purchased this product before.")
}
});
iap.refresh();
}
return (
<IonPage>
<IonContent fullscreen>
<IonCard className="homeCards">
<IonButton onClick={purchaseProduct}>Buy for {productPrice}</IonButton>
<IonButton onClick={restore}>Restore</IonButton>
<IonButton onClick={restore}>Promo code</IonButton>
</IonCard>
</IonContent>
</IonPage>
);
};
export default Home;
To test apple in app purchase on device:
Had a developer account
Created in app purchase in apple connect
Added my email as a tester in apple connect
Uploaded app onto phone from Xcode and purchases worked
To test android in app purchase on device:
<uses-permission android:name="com.android.vending.BILLING" />
Added above line of code to AdnroidManifest.xml file
Had a developer account
Uploaded the .aab or .apk file to internal testing on google play console and added my email as a tester
Created in app purchase
Uploaded app onto phone from Android Studio and purchases worked

Mock multiple fetch calls with state updates in ReactJS

I am having a ReactJS component which does two things:
- on ComponentDidMount it will retrieve a list of entries
- on Button click it will submit the select entry to a backend
The problem is that i need to mock both requests (made with fetch) in order to test it properly. In my current testcase i want to test a failure in the submit on the button click. However due some odd reason the setState is triggered however the update from that is received after i want to compare it.
Dumps i did for the test. First one is the state as listen in the test. The second is from the code itself where it is setting state().error to the error received from the call
FAIL react/src/components/Authentication/DealerSelection.test.jsx (6.689s)
● Console
console.log react/src/components/Authentication/DealerSelection.test.jsx:114
{ loading: true,
error: null,
options: [ { key: 22, value: 22, text: 'Stationstraat 5' } ] }
console.log react/src/components/Authentication/DealerSelection.jsx:52
set error to: my error
The actual test code:
it('throws error message when dealer submit fails', done => {
const mockComponentDidMount = Promise.resolve(
new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
status: 200,
headers: { 'content-type': 'application/json' }
})
);
const mockButtonClickFetchError = Promise.reject(new Error('my error'));
jest.spyOn(global, 'fetch').mockImplementation(() => mockComponentDidMount);
const element = mount(<DealerSelection />);
process.nextTick(() => {
jest.spyOn(global, 'fetch').mockImplementation(() => mockButtonClickFetchError);
const button = element.find('button');
button.simulate('click');
process.nextTick(() => {
console.log(element.state()); // state.error null even though it is set with setState but arrives just after this log statement
global.fetch.mockClear();
done();
});
});
});
This is the component that i actually use:
import React, { Component } from 'react';
import { Form, Header, Select, Button, Banner } from '#omnius/react-ui-elements';
import ClientError from '../../Error/ClientError';
import { fetchBackend } from './service';
import 'whatwg-fetch';
import './DealerSelection.scss';
class DealerSelection extends Component {
state = {
loading: true,
error: null,
dealer: '',
options: []
}
componentDidMount() {
document.title = "Select dealer";
fetchBackend(
'/agent/account/dealerlist',
{},
this.onDealerListSuccessHandler,
this.onFetchErrorHandler
);
}
onDealerListSuccessHandler = json => {
const options = json.data.map((item) => {
return {
key: item.key,
value: item.key,
text: item.value
};
});
this.setState({
loading: false,
options,
dealer: json.default
});
}
onFetchErrorHandler = err => {
if (err instanceof ClientError) {
err.response.json().then(data => {
this.setState({
error: data.error,
loading: false
});
});
} else {
console.log('set error to', err.message);
this.setState({
error: err.message,
loading: false
});
}
}
onSubmitHandler = () => {
const { dealer } = this.state;
this.setState({
loading: true,
error: null
});
fetchBackend(
'/agent/account/dealerPost',
{
dealer
},
this.onDealerSelectSuccessHandler,
this.onFetchErrorHandler
);
}
onDealerSelectSuccessHandler = json => {
if (!json.error) {
window.location = json.redirect; // Refresh to return back to MVC
}
this.setState({
error: json.error
});
}
onChangeHandler = (event, key) => {
this.setState({
dealer: event.target.value
});
}
render() {
const { loading, error, dealer, options } = this.state;
const errorBanner = error ? <Banner type='error' text={error} /> : null;
return (
<div className='dealerselection'>
<Form>
<Header as="h1">Dealer selection</Header>
{ errorBanner }
<Select
label='My dealer'
fluid
defaultValue={dealer}
onChange={this.onChangeHandler}
maxHeight={5}
options={options}
/>
<Button
primary
fluid
onClick={this.onSubmitHandler}
loading={loading}
>Select dealer</Button>
</Form>
</div>
);
}
}
export default DealerSelection;
Interesting, this one took a little while to chase down.
Relevant parts from the Node.js doc on Event Loop, Timers, and process.nextTick():
process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop.
...any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues.
In other words, Node starts processing the nextTickQueue once the current operation is completed, and it will continue until the queue is empty before continuing with the event loop.
This means that if process.nextTick() is called while the nextTickQueue is processing, the callback is added to the queue and it will be processed before the event loop continues.
The doc warns:
This can create some bad situations because it allows you to "starve" your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.
...and as it turns out you can starve your Promise callbacks as well:
test('Promise and process.nextTick order', done => {
const order = [];
Promise.resolve().then(() => { order.push('2') });
process.nextTick(() => {
Promise.resolve().then(() => { order.push('7') });
order.push('3'); // this runs while processing the nextTickQueue...
process.nextTick(() => {
order.push('4'); // ...so all of these...
process.nextTick(() => {
order.push('5'); // ...get processed...
process.nextTick(() => {
order.push('6'); // ...before the event loop continues...
});
});
});
});
order.push('1');
setTimeout(() => {
expect(order).toEqual(['1','2','3','4','5','6','7']); // ...and 7 gets added last
done();
}, 0);
});
So in this case the nested process.nextTick() callback that logs element.state() ends up running before the Promise callbacks that would set state.error to 'my error'.
It is because of this that the doc recommends the following:
We recommend developers use setImmediate() in all cases because it's easier to reason about
If you change your process.nextTick calls to setImmediate (and create your fetch mocks as functions so Promise.reject() doesn't run immediately and cause an error) then your test should work as expected:
it('throws error message when dealer submit fails', done => {
const mockComponentDidMount = () => Promise.resolve(
new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
status: 200,
headers: { 'content-type': 'application/json' }
})
);
const mockButtonClickFetchError = () => Promise.reject(new Error('my error'));
jest.spyOn(global, 'fetch').mockImplementation(mockComponentDidMount);
const element = mount(<DealerSelection />);
setImmediate(() => {
jest.spyOn(global, 'fetch').mockImplementation(mockButtonClickFetchError);
const button = element.find('button');
button.simulate('click');
setImmediate(() => {
console.log(element.state()); // state.error is 'my error'
global.fetch.mockClear();
done();
});
});
});
There are several asynchronous calls required to update the state, so your process.nextTick() isn't sufficient. To update the state, this needs to happen:
your test code clicks, and the event handler callback is queued
the event handler callback runs, runs fetch, gets a promise rejection, and runs the error handler
the error handler runs setState, which queues the state update (setState is asynchronous!)
your test code runs, checking the element's state
the state update runs
In short, you need to wait longer before asserting on the state.
A useful idiom to "wait" without nested process.nextTick() calls is to define a test helper
function wait() {
return new Promise((resolve) => setTimeout(resolve));
}
and then do
await wait();
as many times as required in your test code. Note that this requires you to define test functions as
test(async () => {
})
rather than
test(done => {
})

Resources