BotFramework-WebChat Destroy all bot activity subscriptions on componentWillUnmount - reactjs

I am using the bot framework-webchat 0.12.0 version for a React App. I am initializing the Chat component with custom botconnection, creating a backchannel mechanism.
Does anyone know how to unsubscribe / destroy all bot activities in the backchannel?
When I inspect the network tab it seems that when I navigate with the react router to the view which contains the webchat component, a WebSocket connection is initialized every time and it stays open even after the component has been unmounted (navigate away).
I am afraid those web socket connections might become a problem if they don't get removed.
Here is what I am doing in my React component:
import { Chat, DirectLine } from 'botframework-webchat';
...
componentDidMount() {
const { user, token } = this.props;
if (token && user && Object.keys(user).length !== 0) {
this.botConnection = new DirectLine({
secret: configs.bot.secret_key,
webSocket: 'true'
});
this.initBot();
}
}
initBot = () => {
const { token, user } = this.props;
this.botConnection.postActivity({
type: 'event',
value: {
accessToken: token,
context: 'user'
},
from: {
id: user.userName
},
name: 'conversationInfo'
}).subscribe(() => {
this.setState({ renderChatBot: true });
});
}
...
render() {
const { token, user } = this.props;
if (token !== '' && this.state.renderChatBot) {
console.log('BOTCHAT RENDER');
return (
<Chat
bot={{ id: configs.botId }}
botConnection={this.botConnection}
user={{ id: user.userName }}
token={token}
resize="detect"
/>
);
}
return null;
}
I have spent a few hours searching trough the documentations and also reading trough the files and I can not seem to find a way to destroy those web socket connections when the webchat component get's unmounted.
Any help would be greatly appreciated.

Related

React component uses old data from previous API call

I am using React Query to fetch data from an API I have built. The component is rendering the old data from the previous api call and not updating with new the data from the new api call.
The new data is only rendering when I refresh the page.
Component:
export const ProfilePageStats = (props: {
user: User;
id: number;
}) => {
const { chatId } = useParams();
const { status: subscribeStatus, data: subscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"SUBSCRIBE"
);
const { status: unsubscribeStatus, data: unsubscribeData } =
useSubscriptionsWithType(
chatId ? chatId : "",
props.id,
props.user.id,
"UNSUBSCRIBE"
);
if (unsubscribeStatus == "success" && subscribeStatus == "success") {
console.log("Working", unsubscribeData);
return (
<ProfilePageStatsWithData
user={props.user}
subscribed={Object.keys(subscribeData).length}
unsubscribed={Object.keys(unsubscribeData).length}
/>
);
}
if (unsubscribeStatus == "error" && subscribeStatus == "error") {
console.log("error");
return <ProfilePageStatsLoading />;
}
if (unsubscribeStatus == "loading" && subscribeStatus == "loading") {
console.log("loading");
return <ProfilePageStatsLoading />;
}
return <ProfilePageStatsLoading />;
};
export const useSubscriptionsWithType = (
chatId: string,
id: number,
userId: number,
type: string
) => {
return useQuery(
["subscriptionsWithType"],
async () => {
const { data } = await api.get(
`${chatId}/subscriptions/${id}/${userId}?type=${type}`
);
return data;
},
{
enabled: chatId > 0 && userId > 0,
refetchOnWindowFocus: false,
}
);
};
The component should update to show the new user values but shows the previous user values. If I click out and select a different user entirely it then shows the values for the previously clicked user.
I can see that React Query is fetching with the correct values for the query but the component still renders the old user data?
It turns out that the fetchStatus value is changing to "fetching" but it not actually calling the api. Hence, why its only using the old values?
Your key part of the useQuery is what tells the hook when to update.
You only use ["subscriptionsWithType"] as key, so it will never know that you need to refetch something.
If you add userId there, it will update when that changes.
So, using
return useQuery(
["subscriptionsWithType", userId],
async () => {
...
will work.
It is likely, that you want all the params, that you use in the url, to be added there.
I solved it by adding a useEffect and refetching based on the changing user id.
useEffect(() => {
refetch();
}, [props.user.id]);

socket.io not working in production mode for IOS?

I'm currently using socket.io for real time alerts in my app. I'm developing it using React Native with Expo.
I import this instance of the socket into required components:
socketInstance.js
import io from 'socket.io-client';
import { url } from './url';
const socket = io(url, { secure: true });
export default socket;
And then use it to emit data to the server, for example, when the payment for an order has been completed:
OrderPurchaseScreen.js
const openPaymentSheet = async () => {
const { error } = await presentPaymentSheet();
if (error) {
Alert.alert(`Error code: ${error.code}`, error.message, [
{
text: "Try Again",
onPress: () => openPaymentSheet(),
},
{
text: "Cancel Order",
onPress: () => handleExit(),
style: "cancel",
},
]);
} else {
Alert.alert(
"Payment Successful",
"Your payment has successfully been processed."
);
socket.emit("order-purchase-complete", Store.getState().orderReducer.orderTicket.restaurantId);
setActive(false);
navigation.navigate('OrderCompleteScreen');
}
In the node server
server.js
io.on('connection', (socket) => {
socket.on("logIn", (userId) => {
console.log("new user logged in. - " + userId.toString());
socket.join(userId.toString());
socket.on("order-cancelled", (userId) => {
console.log("order cancelled");
io.to(userId.toString()).emit("order-cancelled", createNotificationObject("Order Cancelled", "The restaurant has cancelled your order. Your money will be refunded."));
});
socket.on("new-order-completed", (userId) => {
console.log("order completed");
io.to(userId.toString()).emit("new-order-completed", createNotificationObject("Order Completed", "Your order has been completed."));
});
});
socket.on("restaurantLogin", (restaurantId) => {
console.log("new restaurant logging in...");
socket.join(restaurantId.toString());
socket.on("new-order-for-approval", (restaurantId) => {
console.log("New Order For Approval!");
io.to(restaurantId.toString()).emit("new-order-for-approval", createNotificationObject("Order Awaiting Approval", "There is a new order awaiting approval. View it in the Restaurant Manager."));
});
socket.on("order-purchase-complete", (restaurantId) => {
console.log("new order purchase completed");
io.to(restaurantId.toString()).emit("order-purchase-complete", createNotificationObject("Order Completed", "A new order has been placed. View it in the Restaurant Manager."));
});
});
}
I have found that in dev mode, everything works fine and as expected. However when I switch to prod mode for IOS (have not tested Android), it only seems to be able to handle the user logging in. When it comes to emitting data after the order being completed for example, nothing gets emitted. Anyone know what I can do to debug this to help me find out the problem or have a potential solution?
Found the answer while browsing the socket.io documentation:
https://socket.io/blog/socket-io-on-ios/
"A Note About Multitasking in iOS
As you probably know, iOS is very picky about what you can do in the background. As such, dont expect that your socket connection will survive in the background! Youll probably stop receiving events within seconds of the app going into the background. So its better to create a task that will gracefully close the connection when it enters the background (via AppDelegate), and then reconnect the socket when the app comes back into the foreground."
So all I did was use AppState to get the state of the app, and depending on if it was in the foreground or background I would re connect to the socket or disconnect:
App.js
useEffect(async () => {
const subscription = AppState.addEventListener(
"change",
async (nextAppState) => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
if (_userToken !== null && email !== null && password !== null) {
socket.connect();
socket.emit("logIn", Store.getState().userReducer._id);
}
appState.current = nextAppState;
setAppStateVisible(appState.current);
if (appState.current === "background") {
socket.disconnect();
}
//console.log("AppState", appState.current);
}
);

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.

production and local routing mismatching

I'm having hard time understanding why my local routing and production routing are mismatching.
I'm writing an app in next.js
In my _app.tsx file I have following lines
if (!isUserLogged && Component.isRestricted) {
redirect(res, '/login');
return { pageProps: {} };
}
if (isUserLogged && !hasUserGivenConsent && !Component.tosPage) {
redirect(res, '/accept-tos');
return { pageProps: {} };
}
if (isUserLogged && hasUserGivenConsent && Component.tosPage) {
redirect(res, '/');
return { pageProps: {} };
}
if (isUserLogged && Component.isAuthPage) {
redirect(res, '/');
return { pageProps: {} };
}
Component.someProperty are always statics in pages/somePage.tsx files.
isUserLoggedIn and hasUserGivenConsent are redux toolkit state.
My pages/login.tsx:
class LoginPage extends Component {
static isRestricted = false;
static isAuthPage = true;
static getInitialProps = wrapper.getInitialPageProps(store => async ({
res,
query: {
token,
uid,
},
}) => {
const { dispatch }: { dispatch: AppThunkDispatch } = store;
if (
!!token && typeof token === 'string' &&
!!uid && typeof uid === 'string'
) {
await dispatch(activateUser({
token,
uid,
}))
.unwrap()
.catch(e => {
redirect(res, '/expired-link ');
});
} else {
dispatch(clearActivationStatus());
}
return {};
});
render() {
return (
<LoginForm />
);
}
}
Locally everything works as expected:
not logged user, trying to access restricted page -> /login
logged user, tos not accepted -> /accept-tos
logged user, tos accepted, trying to access tos page -> /
user clicking on activation mail is directed to /login?uid=...&token=... -> getInitialProps check if there are uid and token if so, then it posts a request to backend to activate account -> if ok user stays on login page -> if not redirects user to /expired-link
Production behavior:
(current problem) no matter if user is logged in or not it redirects to /accept-tos from any URL I type into browser.
(previous problem, not achievable due to current problem) If user clicked on the activation link /login?uid=...&token=... it redirected to / which is restricted path
Nevermind, I ran a gitlab job that cleared the environment so thus it wasn't working.

How to handle response.item is undefined?

I am getting the error below although I bound getNowPlaying, can someone help if I am missing something in my code? Is the way I tried to import SpotifyWebApi wrong?
Here is the code that causes problems:
import React, { Component } from 'react';
import './App.css';
import SpotifyWebApi from 'spotify-web-api-js';
const spotifyApi = new SpotifyWebApi();
class App extends Component {
constructor(props){
super(props);
const params = this.getHashParams();
const token = params.access_token;
if (token) {
spotifyApi.setAccessToken(token);
}
this.state = {
loggedIn: token ? true : false,
nowPlaying: { name: 'Not Checked', albumArt: '' }
}
this.getNowPlaying=this.getNowPlaying.bind(this);
}
getNowPlaying(){
spotifyApi.getMyCurrentPlaybackState()
.then((response) => {
this.setState({
nowPlaying: {
name: response.item.name,
albumArt: response.item.album.images[0].url
}
});
})
}
render() {
return (
<div className="App">
<a href='http://localhost:8888' > Login to Spotify </a>
<div>
Now Playing: { this.state.nowPlaying.name }
</div>
</div>
);
}
}
export default App;
According to Spotify docs
Response A successful request will return a 200 OK response code with
a json payload that contains information about the currently playing
track or episode and its context (see below). The information returned
is for the last known state, which means an inactive device could be
returned if it was the last one to execute playback.
When no available devices are found, the request will return a 200 OK
response but with no data populated.
When no track is currently playing, the request will return a 204 NO
CONTENT response with no payload.
If private session is enabled the response will be a 204 NO CONTENT
with an empty payload.
That means that even if your request is successful (200 OK), the response can be empty so you need to do defensive programming by checking if the response is not empty
I am not sure what is the exact object the request returns but you can do something like this
spotifyApi.getMyCurrentPlaybackState()
.then((response) => {
this.setState({
nowPlaying: {
name: response && response.item && response.item.name,
albumArt: response && response.item && response.item.album && response.item.album.images.length > 0 && response.item.album.images[0].url
}
});
})
The problem here is that the properties you need are deeply nested, and also you need to check if the images array length is greater than 0, so probably a better approach is to store all the response object in the state and then checking it at render time.
if that's not possible you can use optional chaining but for that you will have to use a babel plugin https://babeljs.io/docs/en/babel-plugin-proposal-optional-chaining
And then you can do
spotifyApi.getMyCurrentPlaybackState()
.then((response) => {
this.setState({
nowPlaying: {
name: response?.item?.name,
albumArt: response?.item?.album?.images[0].url
}
});
})

Resources