Skip to content

Commit

Permalink
Farhan/P2PS-1721/order complete toast msg disappeared immediately (#1…
Browse files Browse the repository at this point in the history
…0515)

* fix: order complete toast message disappeares immediately

* chore: keep the notification in notification centre after closing the toast notification

* chore: reduce p2p_order_list calls

* fix: reduce api call and issue notification not updated

* chore: separate logic to hooks

* refactor: 🧹 update test

* fix: 🧹notification missing

* fix: 🧹 failing test

* fix: 🧹 check list length

* fix: 🥸 sort the notifications since backend response could be unsorted

* chore: ✨ separate p2p completed orders reaction to prevent deletion by other reaction

* chore: ✨ checks to update orders info from subscription response

* refactor: 🧹 simplify the code

* fix: 🔧 first order completion disappear

* refactor: 🔧 update comment

* fix: ✨ notification still missing
  • Loading branch information
farhan-nurzi-deriv committed Jan 4, 2024
1 parent a8451d5 commit 59a4968
Show file tree
Hide file tree
Showing 13 changed files with 388 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ const NotificationOrder = ({ action, header, message, onClose }) => {

NotificationOrder.propTypes = {
action: PropTypes.object,
header: PropTypes.string,
header: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
is_auto_close: PropTypes.bool,
message: PropTypes.string,
message: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
onClose: PropTypes.func,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ const Notification = ({ data, removeNotificationMessage }) => {
const destroy = is_closed_by_user => {
removeNotificationMessage(data);

if (data.should_show_again) {
const closed_toast_notifications = JSON.parse(localStorage.getItem('closed_toast_notifications')) ?? [];
if (!closed_toast_notifications.includes(data.key)) {
closed_toast_notifications.push(data.key);
localStorage.setItem('closed_toast_notifications', JSON.stringify(closed_toast_notifications));
}
}

if (data.closeOnClick) {
data.closeOnClick(data, is_closed_by_user);
}
Expand All @@ -29,6 +37,9 @@ const Notification = ({ data, removeNotificationMessage }) => {
setTimeout(destroy, data.delay || default_delay);
}

const closed_toast_notifications = JSON.parse(localStorage.getItem('closed_toast_notifications')) ?? [];
if (closed_toast_notifications.includes(data.key)) return null;

switch (data.type) {
case 'news':
return (
Expand Down Expand Up @@ -160,18 +171,19 @@ Notification.propTypes = {
className: PropTypes.string,
closeOnClick: PropTypes.func,
delay: PropTypes.number,
header: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
header: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
header_popup: PropTypes.string,
img_alt: PropTypes.string,
img_src: PropTypes.string,
is_auto_close: PropTypes.bool,
key: PropTypes.string,
icon: PropTypes.string,
message: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
message: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
message_popup: PropTypes.string,
primary_btn: PropTypes.object,
secondary_btn: PropTypes.object,
should_hide_close_btn: PropTypes.bool,
should_show_again: PropTypes.bool,
size: PropTypes.oneOf(['small']),
timeout: PropTypes.number,
timeoutMessage: PropTypes.func,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { render, screen } from '@testing-library/react';
import Header from '../header';

jest.mock('@deriv/hooks', () => ({
...jest.requireActual('react-router-dom'),
...jest.requireActual('@deriv/hooks'),
useFeatureFlags: jest.fn(() => ({ is_next_wallet_enabled: false })),
useStoreWalletAccountsList: jest.fn(() => ({ data: [], has_wallet: false })),
}));
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/App/Containers/Layout/header/header.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
import { useFeatureFlags, useP2PCompletedOrdersNotification, useStoreWalletAccountsList } from '@deriv/hooks';
import { routes } from '@deriv/shared';
import { observer, useStore } from '@deriv/stores';
import { useFeatureFlags, useStoreWalletAccountsList } from '@deriv/hooks';
import DefaultHeader from './default-header';
import DTraderHeader from './dtrader-header';
import TradersHubHeader from './traders-hub-header';
Expand All @@ -13,6 +13,8 @@ const Header = observer(() => {
const { client } = useStore();
const { accounts, is_logged_in, setAccounts, loginid, switchAccount } = client;
const { pathname } = useLocation();

useP2PCompletedOrdersNotification();
const is_wallets_cashier_route = pathname.includes(routes.wallets_cashier);

const traders_hub_routes =
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/Services/logout.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function endChat() {
const doLogout = response => {
if (response.logout !== 1) return undefined;
removeCookies('affiliate_token', 'affiliate_tracking', 'onfido_token');
localStorage.removeItem('closed_toast_notifications');
SocketCache.clear();
sessionStorage.clear();
endChat();
Expand Down
36 changes: 10 additions & 26 deletions packages/core/src/Stores/notification-store.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import debounce from 'lodash.debounce';
import { action, computed, makeObservable, observable, reaction } from 'mobx';

import { StaticUrl } from '@deriv/components';
Expand Down Expand Up @@ -53,7 +52,7 @@ export default class NotificationStore extends BaseStore {
trade_notifications = [];
p2p_order_props = {};
p2p_redirect_to = {};
p2p_completed_orders = null;
p2p_completed_orders = [];

constructor(root_store) {
super({ root_store });
Expand All @@ -66,7 +65,6 @@ export default class NotificationStore extends BaseStore {
addVerificationNotifications: action.bound,
client_notifications: observable,
filterNotificationMessages: action.bound,
getP2pCompletedOrders: action.bound,
handleClientNotifications: action.bound,
is_notifications_empty: computed,
is_notifications_visible: observable,
Expand Down Expand Up @@ -98,8 +96,6 @@ export default class NotificationStore extends BaseStore {
updateNotifications: action.bound,
});

const debouncedGetP2pCompletedOrders = debounce(this.getP2pCompletedOrders, 1000);

reaction(
() => root_store.common.app_routing_history.map(i => i.pathname),
() => {
Expand All @@ -120,17 +116,7 @@ export default class NotificationStore extends BaseStore {
this.p2p_order_props.order_id,
root_store.client.p2p_advertiser_info,
],
async () => {
if (
root_store.client.is_logged_in &&
!root_store.client.is_virtual &&
Object.keys(root_store.client.account_status || {}).length > 0 &&
Object.keys(root_store.client.landing_companies || {}).length > 0 &&
root_store.client.is_p2p_enabled
) {
await debouncedGetP2pCompletedOrders();
}

() => {
if (
!root_store.client.is_logged_in ||
(Object.keys(root_store.client.account_status || {}).length > 0 &&
Expand All @@ -145,6 +131,12 @@ export default class NotificationStore extends BaseStore {
}
}
);
reaction(
() => this.p2p_completed_orders,
() => {
this.handleClientNotifications();
}
);
}

get is_notifications_empty() {
Expand Down Expand Up @@ -640,6 +632,7 @@ export default class NotificationStore extends BaseStore {
),
platform: 'P2P',
type: 'p2p_completed_order',
should_show_again: true,
});
}

Expand Down Expand Up @@ -682,7 +675,7 @@ export default class NotificationStore extends BaseStore {
this.notification_messages = this.notification_messages.filter(n => n.key !== key);
// Add notification messages to LocalStore when user closes, check for redundancy
const active_loginid = LocalStore.get('active_loginid');
if (!excluded_notifications.includes(key) && active_loginid) {
if (!excluded_notifications.includes(key) && !key.startsWith('p2p_order') && active_loginid) {
let messages = LocalStore.getObject('notification_messages');
// Check if same message already exists in LocalStore for this account
if (messages[active_loginid] && messages[active_loginid].includes(key)) {
Expand Down Expand Up @@ -1617,13 +1610,4 @@ export default class NotificationStore extends BaseStore {
platform: 'Account',
});
};

async getP2pCompletedOrders() {
await WS.wait('authorize');
const response = await WS.send?.({ p2p_order_list: 1, active: 0 });

if (!response?.error) {
this.p2p_completed_orders = response?.p2p_order_list?.list || [];
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import * as React from 'react';
import { useFetch, useSubscription } from '@deriv/api';
import { mockStore, StoreProvider } from '@deriv/stores';
import { renderHook } from '@testing-library/react-hooks';
import useP2PCompletedOrdersNotification from '../useP2PCompletedOrdersNotification';

jest.mock('@deriv/api', () => ({
...jest.requireActual('@deriv/api'),
useFetch: jest.fn(),
useSubscription: jest.fn(),
}));

const mockUseFetch = useFetch as jest.MockedFunction<typeof useFetch<'website_status'>>;
const mockUseSubscription = useSubscription as jest.MockedFunction<typeof useSubscription<'p2p_order_list'>>;

describe('useP2PCompletedOrdersNotification', () => {
test('should unsubscribe from p2p_order_list if user is not authorized', () => {
const mock = mockStore({
client: {
is_authorize: false,
currency: 'USD',
},
notifications: {
p2p_completed_orders: [],
},
});

// @ts-expect-error need to come up with a way to mock the return type of useFetch
mockUseFetch.mockReturnValue({ data: { website_status: { p2p_config: { supported_currencies: ['usd'] } } } });

// @ts-expect-error need to come up with a way to mock the return type of useSubscription
mockUseSubscription.mockReturnValue({
subscribe: jest.fn(),
unsubscribe: jest.fn(),
});

const wrapper = ({ children }: { children: JSX.Element }) => (
<StoreProvider store={mock}>{children}</StoreProvider>
);

renderHook(() => useP2PCompletedOrdersNotification(), { wrapper });

expect(mockUseSubscription('p2p_order_list').unsubscribe).toBeCalled();
expect(mock.notifications.p2p_completed_orders).toEqual([]);
});

test('should unsubscribe from p2p_order_list if user p2p is disabled', () => {
const mock = mockStore({
client: {
is_authorize: false,
currency: 'EUR',
},
notifications: {
p2p_completed_orders: [],
},
});

// @ts-expect-error need to come up with a way to mock the return type of useFetch
mockUseFetch.mockReturnValue({ data: { website_status: { p2p_config: { supported_currencies: ['usd'] } } } });

// @ts-expect-error need to come up with a way to mock the return type of useSubscription
mockUseSubscription.mockReturnValue({
subscribe: jest.fn(),
unsubscribe: jest.fn(),
});

const wrapper = ({ children }: { children: JSX.Element }) => (
<StoreProvider store={mock}>{children}</StoreProvider>
);

renderHook(() => useP2PCompletedOrdersNotification(), { wrapper });

expect(mockUseSubscription('p2p_order_list').unsubscribe).toBeCalled();
expect(mock.notifications.p2p_completed_orders).toEqual([]);
});

test('should subscribe to completed p2p_order_list', () => {
const mock = mockStore({
client: {
is_authorize: true,
currency: 'USD',
},
notifications: {
p2p_completed_orders: [],
},
});

// @ts-expect-error need to come up with a way to mock the return type of useFetch
mockUseFetch.mockReturnValue({ data: { website_status: { p2p_config: { supported_currencies: ['usd'] } } } });

const mock_p2p_order_list = [
{
account_currency: 'USD',
advert_details: {
block_trade: 0,
description: 'Created by script. Please call me 02203400',
id: '75',
payment_method: 'bank_transfer',
type: 'sell',
},
advertiser_details: {
first_name: 'QA script',
id: '38',
is_online: 1,
last_name: 'farhanCpsta',
last_online_time: 1696519153,
loginid: 'CR90000238',
name: 'client CR90000238',
},
amount: 0.1,
amount_display: '0.10',
chat_channel_url: 'p2porder_CR_52_1696518979',
client_details: {
first_name: 'QA script',
id: '39',
is_online: 1,
is_recommended: null,
last_name: 'farhansrjta',
last_online_time: 1696519090,
loginid: 'CR90000239',
name: 'client CR90000239',
},
completion_time: 1696518988,
contact_info: 'Created by script. Please call me 02203400',
created_time: 1696518977,
dispute_details: {
dispute_reason: null,
disputer_loginid: null,
},
expiry_time: 1696522577,
id: '52',
is_incoming: 1,
is_reviewable: 1,
local_currency: 'IDR',
payment_info: 'Transfer to account 000-1111',
price: 1350,
price_display: '1350.00',
rate: 13500,
rate_display: '13500.00',
status: 'completed',
type: 'buy',
},
];

mockUseSubscription.mockReturnValue({
data: {
p2p_order_list: {
// @ts-expect-error need to come up with a way to mock the return type of useSubscription
list: mock_p2p_order_list,
},
},
subscribe: jest.fn(),
unsubscribe: jest.fn(),
});

const wrapper = ({ children }: { children: JSX.Element }) => (
<StoreProvider store={mock}>{children}</StoreProvider>
);

renderHook(() => useP2PCompletedOrdersNotification(), { wrapper });

expect(mockUseSubscription('p2p_order_list').subscribe).toBeCalledWith({
payload: {
active: 0,
},
});
expect(mock.notifications.p2p_completed_orders).toEqual(mock_p2p_order_list);
});
});
Loading

1 comment on commit 59a4968

@vercel
Copy link

@vercel vercel bot commented on 59a4968 Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

deriv-app – ./

deriv-app.vercel.app
deriv-app-git-master.binary.sx
binary.sx
deriv-app.binary.sx

Please sign in to comment.