diff --git a/plugins/src/utils/index.js b/plugins/src/utils/index.js index f5aa416850..95381df1df 100644 --- a/plugins/src/utils/index.js +++ b/plugins/src/utils/index.js @@ -4,151 +4,151 @@ import numbro from 'numbro'; import validator from 'validator'; import isEmail from 'validator/lib/isEmail'; -export const required = value => (!value ? "Required" : undefined); +export const required = value => (!value ? 'Required' : undefined); -export const editableRequired = (string) => (value) => (!value ? (string || "Required") : undefined); +export const editableRequired = (string) => (value) => (!value ? (string || 'Required') : undefined); export const validateOtp = (message) => ( - value = '' + value = '' ) => { - let error = undefined; - if (value.length !== 6 || !validator.isNumeric(value)) { - error = message; - } - return error; + let error = undefined; + if (value.length !== 6 || !validator.isNumeric(value)) { + error = message; + } + return error; }; export const DEFAULT_COIN_DATA = { - fullname: '', - symbol: '', - min: 0.001, + fullname: '', + symbol: '', + min: 0.001, }; const local_base_currnecy = localStorage.getItem('base_currnecy'); export const BASE_CURRENCY = local_base_currnecy - ? local_base_currnecy.toLowerCase() - : 'usdt'; + ? local_base_currnecy.toLowerCase() + : 'usdt'; export const CURRENCY_PRICE_FORMAT = '{0} {1}'; export const formatToCurrency = (amount = 0, min = 0, fullFormat = false) => { - let formatObj = getFormat(min, fullFormat); - return numbro(roundNumber(amount, formatObj.digit)).format(formatObj.format); + let formatObj = getFormat(min, fullFormat); + return numbro(roundNumber(amount, formatObj.digit)).format(formatObj.format); }; export const roundNumber = (number = 0, decimals = 4) => { - if (number === 0) { - return 0; - } else if (decimals > 0) { - const multipliedNumber = math.multiply( - math.fraction(number), - math.pow(10, decimals) - ); - const dividedNumber = math.divide( - math.floor(multipliedNumber), - math.pow(10, decimals) - ); - return math.number(dividedNumber); - } else { - return math.floor(number); - } + if (number === 0 || number === Infinity || isNaN(number)) { + return 0; + } else if (decimals > 0) { + const multipliedNumber = math.multiply( + math.fraction(number), + math.pow(10, decimals) + ); + const dividedNumber = math.divide( + math.floor(multipliedNumber), + math.pow(10, decimals) + ); + return math.number(dividedNumber); + } else { + return math.floor(number); + } }; export const getFormat = (min = 0, fullFormat) => { - let value = math.format(min, { notation: 'fixed' }); - if (fullFormat) { - return { digit: 8, format: '0,0.[00000000]' }; - } else if (min % 1) { - let point = value.toString().split('.')[1] - ? value.toString().split('.')[1] - : ''; - let res = point - .split('') - .map((val) => 0) - .join(''); - return { digit: point.length, format: `0,0.[${res}]` }; - } else { - return { digit: 4, format: `0,0.[0000]` }; - } + let value = math.format(min, { notation: 'fixed' }); + if (fullFormat) { + return { digit: 8, format: '0,0.[00000000]' }; + } else if (min % 1) { + let point = value.toString().split('.')[1] + ? value.toString().split('.')[1] + : ''; + let res = point + .split('') + .map((val) => 0) + .join(''); + return { digit: point.length, format: `0,0.[${res}]` }; + } else { + return { digit: 4, format: '0,0.[0000]' }; + } }; export const getDecimals = (value = 0) => { - let result = math.format(math.number(value), { notation: 'fixed' }); - return value % 1 - ? result.toString().split('.')[1] - ? result.toString().split('.')[1].length - : 0 - : 0; + let result = math.format(math.number(value), { notation: 'fixed' }); + return value % 1 + ? result.toString().split('.')[1] + ? result.toString().split('.')[1].length + : 0 + : 0; }; export const normalizeBTC = (value = 0) => (value ? roundNumber(value, 8) : ''); export const maxValue = (maxValue, message) => (value = 0) => - value > maxValue - ? message - : undefined; + value > maxValue + ? message + : undefined; export const minValue = (minValue, message) => (value = 0) => - value < minValue - ? message - : undefined; + value < minValue + ? message + : undefined; export const checkBalance = (available, message, fee = 0) => (value = 0) => { - const operation = + const operation = fee > 0 - ? math.number( - math.add( - math.fraction(value), - math.multiply(math.fraction(value), math.fraction(fee)) - ) - ) - : value; - - if (operation > available) { - return message; - } - return undefined; + ? math.number( + math.add( + math.fraction(value), + math.multiply(math.fraction(value), math.fraction(fee)) + ) + ) + : value; + + if (operation > available) { + return message; + } + return undefined; }; export const checkBalance_new = (available, message, fee = 0) => (value = 0) => { - const operation = + const operation = fee > 0 - ? math.number( - math.add( - math.fraction(value), - math.fraction(fee) - ) - ) - : value; - - if (operation > available) { - return message; - } - return undefined; + ? math.number( + math.add( + math.fraction(value), + math.fraction(fee) + ) + ) + : value; + + if (operation > available) { + return message; + } + return undefined; }; export const toFixed = (exponential) => { - if (Math.abs(exponential) < 1.0) { - let e = parseInt(exponential.toString().split('e-')[1], 10); - if (e) { - exponential *= Math.pow(10, e - 1); - exponential = + if (Math.abs(exponential) < 1.0) { + let e = parseInt(exponential.toString().split('e-')[1], 10); + if (e) { + exponential *= Math.pow(10, e - 1); + exponential = '0.' + new Array(e).join('0') + exponential.toString().substring(2); - } - } else { - let e = parseInt(exponential.toString().split('+')[1], 10); - if (e > 20) { - e -= 20; - exponential /= Math.pow(10, e); - exponential += new Array(e + 1).join('0'); - } - } - return exponential; + } + } else { + let e = parseInt(exponential.toString().split('+')[1], 10); + if (e > 20) { + e -= 20; + exponential /= Math.pow(10, e); + exponential += new Array(e + 1).join('0'); + } + } + return exponential; }; export const email = (value = '') => - value && !isEmail(value) ? 'Invalid email address' : undefined; + value && !isEmail(value) ? 'Invalid email address' : undefined; -export const maxLength = (length, message) => (value = "") => - value.length > length ? message : undefined; \ No newline at end of file +export const maxLength = (length, message) => (value = '') => + value.length > length ? message : undefined; \ No newline at end of file diff --git a/server/api/controllers/admin.js b/server/api/controllers/admin.js index 979c86f3cd..40d2772743 100644 --- a/server/api/controllers/admin.js +++ b/server/api/controllers/admin.js @@ -5,7 +5,7 @@ const toolsLib = require('hollaex-tools-lib'); const { cloneDeep, pick } = require('lodash'); const { all } = require('bluebird'); const { INIT_CHANNEL, ROLES } = require('../../constants'); -const { USER_NOT_FOUND, API_KEY_NOT_PERMITTED, PROVIDE_VALID_EMAIL, INVALID_PASSWORD, USER_EXISTS, NO_DATA_FOR_CSV, INVALID_VERIFICATION_CODE, INVALID_OTP_CODE } = require('../../messages'); +const { USER_NOT_FOUND, API_KEY_NOT_PERMITTED, PROVIDE_VALID_EMAIL, INVALID_PASSWORD, USER_EXISTS, NO_DATA_FOR_CSV, INVALID_VERIFICATION_CODE, INVALID_OTP_CODE, REFERRAL_HISTORY_NOT_ACTIVE } = require('../../messages'); const { sendEmail, testSendSMTPEmail, sendRawEmail } = require('../../mail'); const { MAILTYPE } = require('../../mail/strings'); const { errorMessageConverter } = require('../../utils/conversion'); @@ -2892,6 +2892,77 @@ const createTradeByAdmin = (req, res) => { return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); }); }; +const getUserReferralCodesByAdmin = (req, res) => { + loggerAdmin.info( + req.uuid, + 'controllers/user/getUserReferralCodesByAdmin', + ); + + const { limit, page, order_by, order, start_date, end_date } = req.swagger.params; + + if ( + !toolsLib.getKitConfig().referral_history_config || + !toolsLib.getKitConfig().referral_history_config.active + ) { + throw new Error(REFERRAL_HISTORY_NOT_ACTIVE); + } + + const user_id = req.swagger.params.user_id.value; + + toolsLib.user.getUserReferralCodes({ + user_id, + limit: limit.value, + page: page.value, + order_by: order_by.value, + order: order.value, + start_date: start_date.value, + end_date: end_date.value + }) + .then((data) => { + return res.json(data); + }) + .catch((err) => { + loggerAdmin.error( + req.uuid, + 'controllers/user/getUserReferralCodesByAdmin err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; +const createUserReferralCodeByAdmin = (req, res) => { + loggerAdmin.info( + req.uuid, + 'controllers/user/createUserReferralCodeByAdmin', + ); + const { user_id, discount, earning_rate, code } = req.swagger.params.data.value; + + if ( + !toolsLib.getKitConfig().referral_history_config || + !toolsLib.getKitConfig().referral_history_config.active + ) { + throw new Error(REFERRAL_HISTORY_NOT_ACTIVE); + } + + toolsLib.user.createUserReferralCode({ + user_id, + discount, + earning_rate, + code, + is_admin: true + }) + .then(() => { + return res.json({ message: 'success' }); + }) + .catch((err) => { + loggerAdmin.error( + req.uuid, + 'controllers/user/createUserReferralCodeByAdmin err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; module.exports = { createInitialAdmin, @@ -2963,5 +3034,7 @@ module.exports = { deleteTransactionLimit, getUserBalanceHistoryByAdmin, createTradeByAdmin, - performDirectWithdrawalByAdmin + performDirectWithdrawalByAdmin, + getUserReferralCodesByAdmin, + createUserReferralCodeByAdmin }; diff --git a/server/api/controllers/p2p.js b/server/api/controllers/p2p.js new file mode 100644 index 0000000000..004b59974a --- /dev/null +++ b/server/api/controllers/p2p.js @@ -0,0 +1,545 @@ +'use strict'; + +const { loggerP2P } = require('../../config/logger'); +const toolsLib = require('hollaex-tools-lib'); +const { errorMessageConverter } = require('../../utils/conversion'); +const { ROLES } = require('../../constants'); +const { API_KEY_NOT_PERMITTED } = require('../../messages'); +const createP2PDeal = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/createP2PDeal/auth', req.auth); + + const { + price_type, + buying_asset, + spending_asset, + exchange_rate, + spread, + total_order_amount, + min_order_value, + max_order_value, + terms, + auto_response, + payment_methods, + region + } = req.swagger.params.data.value; + + loggerP2P.verbose( + req.uuid, + 'controllers/p2p/createP2PDeal data', + price_type, + buying_asset, + spending_asset, + exchange_rate, + spread, + total_order_amount, + min_order_value, + max_order_value, + terms, + auto_response, + region + ); + + toolsLib.p2p.createP2PDeal({ + merchant_id: req.auth.sub.id, + side: 'sell', + price_type, + buying_asset, + spending_asset, + exchange_rate, + spread, + total_order_amount, + min_order_value, + max_order_value, + terms, + auto_response, + payment_methods, + region + } + ) + .then((data) => { + return res.json(data); + }) + .catch((err) => { + loggerP2P.error( + req.uuid, + 'controllers/p2p/createP2PDeal err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const updateP2PDeal = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/updateP2PDeal/auth', req.auth); + + const { + price_type, + buying_asset, + spending_asset, + exchange_rate, + spread, + total_order_amount, + min_order_value, + max_order_value, + terms, + auto_response, + payment_methods, + region, + edited_ids, + status, + id + } = req.swagger.params.data.value; + + loggerP2P.verbose( + req.uuid, + 'controllers/p2p/updateP2PDeal data', + price_type, + buying_asset, + spending_asset, + exchange_rate, + spread, + total_order_amount, + min_order_value, + max_order_value, + terms, + auto_response, + region, + id + ); + + toolsLib.p2p.updateP2PDeal({ + id, + merchant_id: req.auth.sub.id, + edited_ids, + side: 'sell', + price_type, + buying_asset, + spending_asset, + exchange_rate, + spread, + total_order_amount, + min_order_value, + max_order_value, + terms, + auto_response, + payment_methods, + region, + status + } + ) + .then((data) => { + return res.json(data); + }) + .catch((err) => { + loggerP2P.error( + req.uuid, + 'controllers/p2p/updateP2PDeal err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const fetchP2PDeals = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/fetchP2PDeals/auth', req.auth); + + const { user_id, limit, page, order_by, order, start_date, end_date, format, status } = req.swagger.params; + + if (format.value && req.auth.scopes.indexOf(ROLES.ADMIN) === -1) { + return res.status(403).json({ message: API_KEY_NOT_PERMITTED }); + } + + if (order_by.value && typeof order_by.value !== 'string') { + loggerP2P.error( + req.uuid, + 'controllers/p2p/fetchP2PDeals invalid order_by', + order_by.value + ); + return res.status(400).json({ message: 'Invalid order by' }); + } + + toolsLib.p2p.fetchP2PDeals({ + user_id: user_id.value, + limit: limit.value, + page: page.value, + order_by: order_by.value, + order: order.value, + start_date: start_date.value, + end_date: end_date.value, + format: format.value, + status: status.value + } + ) + .then((data) => { + if (format.value === 'csv') { + toolsLib.user.createAuditLog({ email: req?.auth?.sub?.email, session_id: req?.session_id }, req?.swagger?.apiPath, req?.swagger?.operationPath?.[2], req?.swagger?.params); + res.setHeader('Content-disposition', `attachment; filename=${toolsLib.getKitConfig().api_name}-logins.csv`); + res.set('Content-Type', 'text/csv'); + return res.status(202).send(data); + } else { + return res.json(data); + } + }) + .catch((err) => { + loggerP2P.error(req.uuid, 'controllers/p2p/fetchP2PDeals', err.message); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const fetchP2PDisputes = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/fetchP2PDisputes/auth', req.auth); + + const {user_id, limit, page, order_by, order, start_date, end_date, format } = req.swagger.params; + + if (format.value && req.auth.scopes.indexOf(ROLES.ADMIN) === -1) { + return res.status(403).json({ message: API_KEY_NOT_PERMITTED }); + } + + if (order_by.value && typeof order_by.value !== 'string') { + loggerP2P.error( + req.uuid, + 'controllers/p2p/fetchP2PDisputes invalid order_by', + order_by.value + ); + return res.status(400).json({ message: 'Invalid order by' }); + } + + toolsLib.p2p.fetchP2PDisputes({ + user_id: user_id.value, + limit: limit.value, + page: page.value, + order_by: order_by.value, + order: order.value, + start_date: start_date.value, + end_date: end_date.value, + format: format.value, + } + ) + .then((data) => { + if (format.value === 'csv') { + toolsLib.user.createAuditLog({ email: req?.auth?.sub?.email, session_id: req?.session_id }, req?.swagger?.apiPath, req?.swagger?.operationPath?.[2], req?.swagger?.params); + res.setHeader('Content-disposition', `attachment; filename=${toolsLib.getKitConfig().api_name}-logins.csv`); + res.set('Content-Type', 'text/csv'); + return res.status(202).send(data); + } else { + return res.json(data); + } + }) + .catch((err) => { + loggerP2P.error(req.uuid, 'controllers/p2p/fetchP2PDisputes', err.message); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + + +const fetchP2PTransactions = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/fetchP2PTransactions/auth', req.auth); + + const { id, limit, page, order_by, order, start_date, end_date, format } = req.swagger.params; + + if (format.value && req.auth.scopes.indexOf(ROLES.ADMIN) === -1) { + return res.status(403).json({ message: API_KEY_NOT_PERMITTED }); + } + + if (order_by.value && typeof order_by.value !== 'string') { + loggerP2P.error( + req.uuid, + 'controllers/p2p/fetchP2PTransactions invalid order_by', + order_by.value + ); + return res.status(400).json({ message: 'Invalid order by' }); + } + + toolsLib.p2p.fetchP2PTransactions(req?.auth?.sub?.id, { + id: id.value, + limit: limit.value, + page: page.value, + order_by: order_by.value, + order: order.value, + start_date: start_date.value, + end_date: end_date.value, + format: format.value + } + ) + .then((data) => { + if (format.value === 'csv') { + toolsLib.user.createAuditLog({ email: req?.auth?.sub?.email, session_id: req?.session_id }, req?.swagger?.apiPath, req?.swagger?.operationPath?.[2], req?.swagger?.params); + res.setHeader('Content-disposition', `attachment; filename=${toolsLib.getKitConfig().api_name}-logins.csv`); + res.set('Content-Type', 'text/csv'); + return res.status(202).send(data); + } else { + return res.json(data); + } + }) + .catch((err) => { + loggerP2P.error(req.uuid, 'controllers/p2p/fetchP2PTransactions', err.message); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const createP2PTransaction = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/createP2PTransaction/auth', req.auth); + const ip = req.headers['x-real-ip']; + const { + deal_id, + amount_fiat, + payment_method_used + } = req.swagger.params.data.value; + + loggerP2P.verbose( + req.uuid, + 'controllers/p2p/createP2PTransaction data', + deal_id, + amount_fiat, + payment_method_used + ); + + toolsLib.p2p.createP2PTransaction({ + deal_id, + user_id: req.auth.sub.id, + amount_fiat, + payment_method_used, + ip + } + ) + .then((data) => { + return res.json(data); + }) + .catch((err) => { + loggerP2P.error( + req.uuid, + 'controllers/p2p/createP2PTransaction err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const updateP2PTransaction = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/updateP2PTransaction/auth', req.auth); + const ip = req.headers['x-real-ip']; + const { + id, + user_status, + merchant_status, + cancellation_reason, + + } = req.swagger.params.data.value; + + loggerP2P.verbose( + req.uuid, + 'controllers/p2p/updateP2PTransaction data', + id, + user_status, + merchant_status, + cancellation_reason, + ); + + toolsLib.p2p.updateP2pTransaction({ + user_id: req.auth.sub.id, + id, + user_status, + merchant_status, + cancellation_reason, + ip + } + ) + .then((data) => { + return res.json(data); + }) + .catch((err) => { + loggerP2P.error( + req.uuid, + 'controllers/p2p/updateP2PTransaction err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +} +const updateP2PDispute = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/updateP2PDispute/auth', req.auth); + + const { + id, + resolution, + status, + } = req.swagger.params.data.value; + + loggerP2P.verbose( + req.uuid, + 'controllers/p2p/updateP2PDispute data', + id, + resolution, + status, + ); + + toolsLib.p2p.updateP2pDispute({ + id, + resolution, + status, + } + ) + .then((data) => { + return res.json(data); + }) + .catch((err) => { + loggerP2P.error( + req.uuid, + 'controllers/p2p/updateP2PDispute err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const createP2pChatMessage = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/createP2pChatMessage/auth', req.auth); + + const { + receiver_id, + message, + transaction_id + } = req.swagger.params.data.value; + + loggerP2P.verbose( + req.uuid, + 'controllers/p2p/createP2pChatMessage data', + receiver_id, + message, + transaction_id + + ); + + toolsLib.p2p.createP2pChatMessage({ + sender_id: req.auth.sub.id, + receiver_id, + transaction_id, + message, + } + ) + .then((data) => { + return res.json(data); + }) + .catch((err) => { + loggerP2P.error( + req.uuid, + 'controllers/p2p/createP2pChatMessage err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const createP2PFeedback = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/createP2PFeedback/auth', req.auth); + + const { + transaction_id, + comment, + rating + } = req.swagger.params.data.value; + + loggerP2P.verbose( + req.uuid, + 'controllers/p2p/createP2PFeedback data', + transaction_id, + comment, + rating + ); + + toolsLib.p2p.createMerchantFeedback({ + user_id: req.auth.sub.id, + transaction_id, + comment, + rating + } + ) + .then((data) => { + return res.json(data); + }) + .catch((err) => { + loggerP2P.error( + req.uuid, + 'controllers/p2p/createP2PFeedback err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const fetchP2PFeedbacks = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/fetchP2PFeedbacks/auth', req.auth); + + const { transaction_id, merchant_id, limit, page, order_by, order, start_date, end_date, format } = req.swagger.params; + + if (format.value && req.auth.scopes.indexOf(ROLES.ADMIN) === -1) { + return res.status(403).json({ message: API_KEY_NOT_PERMITTED }); + } + + if (order_by.value && typeof order_by.value !== 'string') { + loggerP2P.error( + req.uuid, + 'controllers/p2p/fetchP2PFeedbacks invalid order_by', + order_by.value + ); + return res.status(400).json({ message: 'Invalid order by' }); + } + + toolsLib.p2p.fetchP2PFeedbacks({ + transaction_id: transaction_id.value, + merchant_id: merchant_id.value, + limit: limit.value, + page: page.value, + order_by: order_by.value, + order: order.value, + start_date: start_date.value, + end_date: end_date.value, + format: format.value, + } + ) + .then((data) => { + if (format.value === 'csv') { + toolsLib.user.createAuditLog({ email: req?.auth?.sub?.email, session_id: req?.session_id }, req?.swagger?.apiPath, req?.swagger?.operationPath?.[2], req?.swagger?.params); + res.setHeader('Content-disposition', `attachment; filename=${toolsLib.getKitConfig().api_name}-logins.csv`); + res.set('Content-Type', 'text/csv'); + return res.status(202).send(data); + } else { + return res.json(data); + } + }) + .catch((err) => { + loggerP2P.error(req.uuid, 'controllers/p2p/fetchP2PFeedbacks', err.message); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const fetchP2PProfile = (req, res) => { + loggerP2P.verbose(req.uuid, 'controllers/p2p/fetchP2PProfile/auth', req.auth); + + const { user_id } = req.swagger.params; + + + toolsLib.p2p.fetchP2PProfile(user_id.value) + .then((data) => { + return res.json(data); + }) + .catch((err) => { + loggerP2P.error(req.uuid, 'controllers/p2p/fetchP2PProfile', err.message); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +module.exports = { + createP2PDeal, + fetchP2PDeals, + fetchP2PTransactions, + createP2PTransaction, + createP2pChatMessage, + updateP2PTransaction, + fetchP2PDisputes, + updateP2PDeal, + updateP2PDispute, + createP2PFeedback, + fetchP2PFeedbacks, + fetchP2PProfile +}; \ No newline at end of file diff --git a/server/api/controllers/user.js b/server/api/controllers/user.js index 55c6cdc436..01054aa27c 100644 --- a/server/api/controllers/user.js +++ b/server/api/controllers/user.js @@ -1269,6 +1269,213 @@ const fetchUserProfitLossInfo = (req, res) => { }); }; +const getUnrealizedUserReferral = (req, res) => { + loggerUser.info( + req.uuid, + 'controllers/user/getUnrealizedUserReferral', + ); + + if ( + !toolsLib.getKitConfig().referral_history_config || + !toolsLib.getKitConfig().referral_history_config.active + ) { + // TODO it should be added to the messages + throw new Error('Feature is not active'); + } + + toolsLib.user.getUnrealizedReferral(req.auth.sub.id) + .then((data) => { + return res.json({ data }); + }) + .catch((err) => { + loggerUser.error( + req.uuid, + 'controllers/user/getUnrealizedUserReferral err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const getRealizedUserReferral = (req, res) => { + loggerUser.verbose(req.uuid, 'controllers/user/getRealizedUserReferral/auth', req.auth); + + const { limit, page, order_by, order, start_date, end_date, format } = req.swagger.params; + + if (order_by.value && typeof order_by.value !== 'string') { + loggerUser.error( + req.uuid, + 'controllers/user/getRealizedUserReferral invalid order_by', + order_by.value + ); + return res.status(400).json({ message: 'Invalid order by' }); + } + + toolsLib.user.getRealizedReferral({ + user_id: req.auth.sub.id, + limit: limit.value, + page: page.value, + order_by: order_by.value, + order: order.value, + start_date: start_date.value, + end_date: end_date.value, + format: format.value + } + ) + .then((data) => { + if (format.value === 'csv') { + toolsLib.user.createAuditLog({ email: req?.auth?.sub?.email, session_id: req?.session_id }, req?.swagger?.apiPath, req?.swagger?.operationPath?.[2], req?.swagger?.params); + res.setHeader('Content-disposition', `attachment; filename=${toolsLib.getKitConfig().api_name}-logins.csv`); + res.set('Content-Type', 'text/csv'); + return res.status(202).send(data); + } else { + return res.json(data); + } + }) + .catch((err) => { + loggerUser.error(req.uuid, 'controllers/user/getRealizedUserReferral', err.message); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const getUserReferralCodes = (req, res) => { + loggerUser.info( + req.uuid, + 'controllers/user/getUserReferralCodes', + ); + + if ( + !toolsLib.getKitConfig().referral_history_config || + !toolsLib.getKitConfig().referral_history_config.active + ) { + // TODO it should be added to the messages + throw new Error('Feature is not active'); + } + + toolsLib.user.getUserReferralCodes({ user_id: req.auth.sub.id }) + .then((data) => { + return res.json({ data }); + }) + .catch((err) => { + loggerUser.error( + req.uuid, + 'controllers/user/getUserReferralCodes err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const createUserReferralCode = (req, res) => { + loggerUser.info( + req.uuid, + 'controllers/user/createUserReferralCode', + ); + const { discount, earning_rate, code } = req.swagger.params.data.value; + + if ( + !toolsLib.getKitConfig().referral_history_config || + !toolsLib.getKitConfig().referral_history_config.active + ) { + // TODO it should be added to the messages + throw new Error('Feature is not active'); + } + + toolsLib.user.createUserReferralCode({ + user_id: req.auth.sub.id, + discount, + earning_rate, + code + }) + .then(() => { + return res.json({ message: 'success' }); + }) + .catch((err) => { + loggerUser.error( + req.uuid, + 'controllers/user/createUserReferralCode err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const settleUserFees = (req, res) => { + loggerUser.info( + req.uuid, + 'controllers/user/settleUserFees', + ); + + if ( + !toolsLib.getKitConfig().referral_history_config || + !toolsLib.getKitConfig().referral_history_config.active + ) { + // TODO it should be added to the messages + throw new Error('Feature is not active'); + } + + toolsLib.user.settleFees(req.auth.sub.id) + .then(() => { + return res.json({ message: 'success' }); + }) + .catch((err) => { + loggerUser.error( + req.uuid, + 'controllers/user/settleUserFees err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + +const fetchUserReferrals = (req, res) => { + const { limit, page, order_by, order, start_date, end_date, format } = req.swagger.params; + + loggerUser.info( + req.uuid, + 'controllers/user/referrals', + limit, + page, + order_by, + order, + start_date, + end_date, + format + ); + + if ( + !toolsLib.getKitConfig().referral_history_config || + !toolsLib.getKitConfig().referral_history_config.active + ) { + // TODO it should be added to the messages + throw new Error('Feature is not active'); + } + + toolsLib.user.fetchUserReferrals( + { + user_id: req.auth.sub.id, + limit: limit.value, + page: page.value, + order_by: order_by.value, + order: order.value, + start_date: start_date.value, + end_date: end_date.value, + format: format.value + } + ) + .then((referrals) => { + return res.json(referrals); + }) + .catch((err) => { + loggerUser.error( + req.uuid, + 'controllers/user/referrals err', + err.message + ); + return res.status(err.statusCode || 400).json({ message: errorMessageConverter(err) }); + }); +}; + module.exports = { signUpUser, @@ -1300,6 +1507,12 @@ module.exports = { getUserSessions, userLogout, userDelete, + getUnrealizedUserReferral, + getRealizedUserReferral, + settleUserFees, getUserBalanceHistory, - fetchUserProfitLossInfo -}; + fetchUserProfitLossInfo, + fetchUserReferrals, + createUserReferralCode, + getUserReferralCodes +}; \ No newline at end of file diff --git a/server/api/swagger/admin.yaml b/server/api/swagger/admin.yaml index 743bcdee61..97d59d3792 100644 --- a/server/api/swagger/admin.yaml +++ b/server/api/swagger/admin.yaml @@ -3354,6 +3354,115 @@ paths: x-security-scopes: - admin - supervisor + /admin/user/referral/code: + x-swagger-router-controller: admin + get: + description: Get user referral codes for admin + operationId: getUserReferralCodesByAdmin + tags: + - User + parameters: + - name: user_id + in: query + required: false + type: number + - in: query + name: limit + description: "Number of elements to return. Default: 50. Maximun: 100" + required: false + type: number + format: int32 + - in: query + name: page + description: Page of data to retrieve + required: false + type: number + format: int32 + - in: query + name: order_by + description: Field to order data + required: false + type: string + - in: query + name: order + description: direction to order + required: false + type: string + enum: ['asc', 'desc'] + - in: query + name: start_date + description: Starting date of queried data + required: false + type: string + format: date-time + - in: query + name: end_date + description: Ending date of queried data + required: false + type: string + format: date-time + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + 202: + description: CSV + schema: + type: string + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + x-security-scopes: + - admin + x-token-permissions: + - can_read + post: + operationId: createUserReferralCodeByAdmin + description: create referral code config for user by admin + tags: + - User + parameters: + - name: data + in: body + required: true + schema: + type: object + required: + - user_id + - discount + - earning_rate + - code + properties: + discount: + type: number + format: double + earning_rate: + type: number + format: double + code: + type: string + maxLength: 256 + responses: + 200: + description: Success + schema: + $ref: "#/definitions/MessageResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + x-security-scopes: + - admin /admin/quicktrade/config: x-swagger-router-controller: admin put: @@ -3837,6 +3946,118 @@ paths: - admin x-token-permissions: - can_read + /admin/p2p/dispute: + x-swagger-router-controller: p2p + get: + description: Get p2p disputes + operationId: fetchP2PDisputes + tags: + - User + parameters: + - in: query + name: user_id + description: "user_id" + required: false + type: number + format: int32 + - in: query + name: limit + description: "Number of elements to return. Default: 50. Maximun: 100" + required: false + type: number + format: int32 + - in: query + name: page + description: Page of data to retrieve + required: false + type: number + format: int32 + - in: query + name: order_by + description: Field to order data + required: false + type: string + - in: query + name: order + description: direction to order + required: false + type: string + enum: ['asc', 'desc'] + - in: query + name: start_date + description: Starting date of queried data + required: false + type: string + format: date-time + - in: query + name: end_date + description: Ending date of queried data + required: false + type: string + format: date-time + - in: query + name: format + description: Specify data format + required: false + enum: ['csv', 'all'] + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + 202: + description: CSV + schema: + type: string + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - admin + x-token-permissions: + - can_read + put: + description: Update dispute for p2p + operationId: updateP2PDispute + tags: + - P2P + parameters: + - name: data + in: body + required: true + schema: + type: object + properties: + id: + type: integer + format: int32 + resolution: + type: string + status: + type: boolean + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - admin /admin/trade: x-swagger-router-controller: admin post: diff --git a/server/api/swagger/p2p.yaml b/server/api/swagger/p2p.yaml new file mode 100644 index 0000000000..90d3b6522b --- /dev/null +++ b/server/api/swagger/p2p.yaml @@ -0,0 +1,554 @@ +paths: + /p2p/deal: + x-swagger-router-controller: p2p + get: + description: Get p2p deals + operationId: fetchP2PDeals + tags: + - User + parameters: + - in: query + name: user_id + description: "user_id" + required: false + type: number + format: int32 + - in: query + name: status + description: "status" + required: false + type: boolean + - in: query + name: limit + description: "Number of elements to return. Default: 50. Maximun: 100" + required: false + type: number + format: int32 + - in: query + name: page + description: Page of data to retrieve + required: false + type: number + format: int32 + - in: query + name: order_by + description: Field to order data + required: false + type: string + - in: query + name: order + description: direction to order + required: false + type: string + enum: ['asc', 'desc'] + - in: query + name: start_date + description: Starting date of queried data + required: false + type: string + format: date-time + - in: query + name: end_date + description: Ending date of queried data + required: false + type: string + format: date-time + - in: query + name: format + description: Specify data format + required: false + enum: ['csv', 'all'] + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + 202: + description: CSV + schema: + type: string + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + post: + description: Create deal for p2p + operationId: createP2PDeal + tags: + - P2P + parameters: + - name: data + in: body + required: true + schema: + type: object + properties: + price_type: + type: string + buying_asset: + type: string + spending_asset: + type: string + exchange_rate: + type: number + format: double + spread: + type: number + format: double + total_order_amount: + type: number + format: double + min_order_value: + type: number + format: double + max_order_value: + type: number + format: double + terms: + type: string + auto_response: + type: string + payment_methods: + type: array + items: + type: object + region: + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - user + put: + description: Update deal for p2p + operationId: updateP2PDeal + tags: + - P2P + parameters: + - name: data + in: body + required: true + schema: + type: object + properties: + price_type: + type: string + buying_asset: + type: string + spending_asset: + type: string + exchange_rate: + type: number + format: double + spread: + type: number + format: double + total_order_amount: + type: number + format: double + min_order_value: + type: number + format: double + max_order_value: + type: number + format: double + terms: + type: string + auto_response: + type: string + payment_methods: + type: array + items: + type: object + region: + type: string + edited_ids: + type: array + items: + type: integer + format: int32 + status: + type: boolean + id: + type: integer + format: int32 + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - user + /p2p/order: + x-swagger-router-controller: p2p + get: + description: Get p2p transactions + operationId: fetchP2PTransactions + tags: + - User + parameters: + - in: query + name: user_id + description: "user_id" + required: false + type: number + format: int32 + - in: query + name: id + description: transaction id + required: false + type: number + format: int32 + - in: query + name: limit + description: "Number of elements to return. Default: 50. Maximun: 100" + required: false + type: number + format: int32 + - in: query + name: page + description: Page of data to retrieve + required: false + type: number + format: int32 + - in: query + name: order_by + description: Field to order data + required: false + type: string + - in: query + name: order + description: direction to order + required: false + type: string + enum: ['asc', 'desc'] + - in: query + name: start_date + description: Starting date of queried data + required: false + type: string + format: date-time + - in: query + name: end_date + description: Ending date of queried data + required: false + type: string + format: date-time + - in: query + name: format + description: Specify data format + required: false + enum: ['csv', 'all'] + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + 202: + description: CSV + schema: + type: string + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - user + x-token-permissions: + - can_read + post: + description: Create transaction for p2p + operationId: createP2PTransaction + tags: + - P2P + parameters: + - name: data + in: body + required: true + schema: + type: object + properties: + deal_id: + type: integer + format: int32 + amount_fiat: + type: number + format: double + payment_method_used: + type: object + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - user + put: + description: update transaction for p2p + operationId: updateP2PTransaction + tags: + - P2P + parameters: + - name: data + in: body + required: true + schema: + type: object + properties: + id: + type: integer + format: int32 + user_status: + type: string + merchant_status: + type: string + cancellation_reason: + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - user + /p2p/order/chat: + x-swagger-router-controller: p2p + post: + description: Create message for transaction chat + operationId: createP2pChatMessage + tags: + - P2P + parameters: + - name: data + in: body + required: true + schema: + type: object + properties: + receiver_id: + type: integer + format: int32 + message: + type: string + transaction_id: + type: integer + format: int32 + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - user + /p2p/feedback: + x-swagger-router-controller: p2p + get: + description: Get p2p feedback + operationId: fetchP2PFeedbacks + tags: + - User + parameters: + - in: query + name: transaction_id + description: "transaction_id" + required: false + type: number + format: int32 + - in: query + name: merchant_id + description: "id for merchant" + required: false + type: number + format: int32 + - in: query + name: limit + description: "Number of elements to return. Default: 50. Maximun: 100" + required: false + type: number + format: int32 + - in: query + name: page + description: Page of data to retrieve + required: false + type: number + format: int32 + - in: query + name: order_by + description: Field to order data + required: false + type: string + - in: query + name: order + description: direction to order + required: false + type: string + enum: ['asc', 'desc'] + - in: query + name: start_date + description: Starting date of queried data + required: false + type: string + format: date-time + - in: query + name: end_date + description: Ending date of queried data + required: false + type: string + format: date-time + - in: query + name: format + description: Specify data format + required: false + enum: ['csv', 'all'] + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + 202: + description: CSV + schema: + type: string + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - user + x-token-permissions: + - can_read + post: + description: create feedback for p2p + operationId: createP2PFeedback + tags: + - P2P + parameters: + - name: data + in: body + required: true + schema: + type: object + required: + - comment + - rating + - transaction_id + properties: + rating: + type: integer + format: int32 + transaction_id: + type: integer + format: int32 + comment: + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - user +/p2p/profile: + x-swagger-router-controller: p2p + get: + description: Get p2p feedback + operationId: fetchP2PProfile + tags: + - User + parameters: + - in: query + name: user_id + description: "user_id" + required: false + type: number + format: int32 + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + 202: + description: CSV + schema: + type: string + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + x-security-scopes: + - user + x-token-permissions: + - can_read \ No newline at end of file diff --git a/server/api/swagger/swagger.js b/server/api/swagger/swagger.js index 9eedc35427..1401fbcc55 100644 --- a/server/api/swagger/swagger.js +++ b/server/api/swagger/swagger.js @@ -4,7 +4,7 @@ const definition = { swagger: '2.0', info: { title: 'HollaEx Kit', - version: '2.10.4' + version: '2.11.0' }, host: 'api.hollaex.com', basePath: '/v2', diff --git a/server/api/swagger/user.yaml b/server/api/swagger/user.yaml index dee417d7fa..6b6f93e699 100644 --- a/server/api/swagger/user.yaml +++ b/server/api/swagger/user.yaml @@ -1496,6 +1496,259 @@ paths: - bearer x-security-scopes: - user + /user/referral/code: + x-swagger-router-controller: user + get: + description: Get user referral codes + operationId: getUserReferralCodes + tags: + - User + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + 202: + description: CSV + schema: + type: string + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + x-security-scopes: + - user + x-token-permissions: + - can_read + post: + operationId: createUserReferralCode + description: create referral code config for user + tags: + - User + parameters: + - name: data + in: body + required: true + schema: + type: object + required: + - discount + - earning_rate + - code + properties: + discount: + type: number + format: double + earning_rate: + type: number + format: double + code: + type: string + maxLength: 256 + responses: + 200: + description: Success + schema: + $ref: "#/definitions/MessageResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + x-security-scopes: + - user + /user/referral/unrealized: + x-swagger-router-controller: user + get: + description: Get user unrealized fee earnings + operationId: getUnrealizedUserReferral + tags: + - User + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + - hmac + x-security-scopes: + - user + x-token-permissions: + - can_read + post: + operationId: settleUserFees + description: settle user fees + tags: + - User + responses: + 200: + description: Success + schema: + $ref: "#/definitions/MessageResponse" + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + x-security-scopes: + - user + /user/referral/realized: + x-swagger-router-controller: user + get: + description: Get user realized fee earnings + operationId: getRealizedUserReferral + tags: + - User + parameters: + - in: query + name: limit + description: "Number of elements to return. Default: 50. Maximun: 100" + required: false + type: number + format: int32 + - in: query + name: page + description: Page of data to retrieve + required: false + type: number + format: int32 + - in: query + name: order_by + description: Field to order data + required: false + type: string + - in: query + name: order + description: direction to order + required: false + type: string + enum: ['asc', 'desc'] + - in: query + name: start_date + description: Starting date of queried data + required: false + type: string + format: date-time + - in: query + name: end_date + description: Ending date of queried data + required: false + type: string + format: date-time + - in: query + name: format + description: Specify data format + required: false + enum: ['csv', 'all'] + type: string + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + 202: + description: CSV + schema: + type: string + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + x-security-scopes: + - user + x-token-permissions: + - can_read + /user/referral/history: + x-swagger-router-controller: user + get: + description: Get user referral history + operationId: fetchUserReferrals + tags: + - User + parameters: + - in: query + name: limit + description: "Number of elements to return. Default: 50. Maximun: 100" + required: false + type: number + format: int32 + - in: query + name: page + description: Page of data to retrieve + required: false + type: number + format: int32 + - in: query + name: order_by + description: Field to order data + required: false + type: string + - in: query + name: order + description: direction to order + required: false + type: string + enum: ['asc', 'desc'] + - in: query + name: start_date + description: Starting date of queried data + required: false + type: string + format: date-time + - in: query + name: end_date + description: Ending date of queried data + required: false + type: string + format: date-time + - in: query + name: format + description: format + required: false + type: string + enum: ['all'] + responses: + 200: + description: Success + schema: + $ref: "#/definitions/ObjectResponse" + 202: + description: CSV + schema: + type: string + default: + description: Error + schema: + $ref: "#/definitions/MessageResponse" + security: + - Token: [] + x-security-types: + - bearer + x-security-scopes: + - user + x-token-permissions: + - can_read /logout: x-swagger-router-controller: user get: diff --git a/server/config/logger.js b/server/config/logger.js index bc953de563..9014c6f5fb 100644 --- a/server/config/logger.js +++ b/server/config/logger.js @@ -101,6 +101,7 @@ const LOGGER_NAMES = { init: 'init', broker: 'broker', stake: 'stake', + p2p: 'p2p', fiat: 'fiat' }; @@ -159,5 +160,6 @@ module.exports = { loggerTier: winston.loggers.get(LOGGER_NAMES.tier), loggerBroker: winston.loggers.get(LOGGER_NAMES.broker), loggerStake: winston.loggers.get(LOGGER_NAMES.stake), + loggerP2P: winston.loggers.get(LOGGER_NAMES.p2p), loggerFiat: winston.loggers.get(LOGGER_NAMES.loggerFiat) }; diff --git a/server/constants.js b/server/constants.js index adb3d86c14..46a67b53df 100644 --- a/server/constants.js +++ b/server/constants.js @@ -218,6 +218,8 @@ exports.KIT_CONFIG_KEYS = [ 'fiat_fees', 'balance_history_config', 'transaction_limits', + 'p2p_config', + 'referral_history_config' ]; exports.KIT_SECRETS_KEYS = [ @@ -288,6 +290,8 @@ exports.WEBSOCKET_CHANNEL = (topic, symbolOrUserId) => { return 'admin'; case 'chat': return 'chat'; + case 'p2pChat': + return `p2pChat:${symbolOrUserId}`; default: return; } @@ -299,6 +303,7 @@ exports.WS_HUB_CHANNEL = 'channel:websocket:hub'; // Chat exports.CHAT_MAX_MESSAGES = 50; exports.CHAT_MESSAGE_CHANNEL = 'channel:chat:message'; +exports.P2P_CHAT_MESSAGE_CHANNEL = 'channel:p2p'; // CHANNEL CONSTANTS END -------------------------------------------------- @@ -664,10 +669,21 @@ exports.STAKE_SUPPORTED_PLANS = ['fiat', 'boost', 'enterprise']; //STAKE CONSTANTS END +//P2P CONSTANTS START + +exports.P2P_SUPPORTED_PLANS = ['fiat', 'boost', 'enterprise']; + +//P2P CONSTANTS END + //BALANCE HISTORY CONSTANTS START exports.BALANCE_HISTORY_SUPPORTED_PLANS = ['fiat', 'boost', 'enterprise']; //BALANCE HISTORY CONSTANTS END +//REFERRAL HISTORY CONSTANTS START + +exports.REFERRAL_HISTORY_SUPPORTED_PLANS = ['fiat', 'boost', 'enterprise']; +//REFERRAL HISTORY CONSTANTS END + exports.CUSTOM_CSS = ` .topbar-wrapper img { content:url('${exports.GET_KIT_CONFIG().logo_image}}'); @@ -691,4 +707,4 @@ exports.CUSTOM_CSS = ` .models { display: none !important; } -`; +`; \ No newline at end of file diff --git a/server/db/migrations/20231228156527-create-referral-history.js b/server/db/migrations/20231228156527-create-referral-history.js new file mode 100644 index 0000000000..1f0060dea7 --- /dev/null +++ b/server/db/migrations/20231228156527-create-referral-history.js @@ -0,0 +1,66 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('ReferralHistories', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + referer: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + referee: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + code: { + type: Sequelize.STRING, + allowNull: true, + }, + coin: { + type: Sequelize.STRING, + allowNull: false + }, + accumulated_fees: { + type: Sequelize.DOUBLE, + allowNull: false + }, + status: { + type: Sequelize.BOOLEAN, + allowNull: false + }, + last_settled: { + type: Sequelize.DATE, + allowNull: false + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()') + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()') + } + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('ReferralHistories'); + }, +}; \ No newline at end of file diff --git a/server/db/migrations/20240215163266-create-p2p-deal.js b/server/db/migrations/20240215163266-create-p2p-deal.js new file mode 100644 index 0000000000..654961be0c --- /dev/null +++ b/server/db/migrations/20240215163266-create-p2p-deal.js @@ -0,0 +1,97 @@ +'use strict'; + +const TABLE = 'P2pDeals'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable(TABLE, { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + merchant_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + side: { + type: Sequelize.STRING, + allowNull: false, + }, + price_type: { + type: Sequelize.ENUM('static', 'dynamic'), + allowNull: false, + }, + buying_asset: { + type: Sequelize.STRING, + allowNull: false, + }, + spending_asset: { + type: Sequelize.STRING, + allowNull: false, + }, + exchange_rate: { + type: Sequelize.DOUBLE, + allowNull: true, + }, + spread: { + type: Sequelize.DOUBLE, + allowNull: true, + }, + total_order_amount: { + type: Sequelize.DOUBLE, + allowNull: false, + }, + min_order_value: { + type: Sequelize.DOUBLE, + allowNull: false, + }, + max_order_value: { + type: Sequelize.DOUBLE, + allowNull: false, + }, + terms: { + type: Sequelize.TEXT, + allowNull: true, + }, + auto_response: { + type: Sequelize.STRING, + allowNull: true, + }, + region: { + type: Sequelize.STRING, + allowNull: true, + }, + payment_methods: { + type: Sequelize.JSONB, + allowNull: false, + }, + status: { + type: Sequelize.BOOLEAN, + allowNull: false, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }, + { + timestamps: true, + underscored: true + } + ); + }, + down: (queryInterface) => queryInterface.dropTable(TABLE), +}; diff --git a/server/db/migrations/20240215163267-create-p2p-transaction.js b/server/db/migrations/20240215163267-create-p2p-transaction.js new file mode 100644 index 0000000000..51bbb5102d --- /dev/null +++ b/server/db/migrations/20240215163267-create-p2p-transaction.js @@ -0,0 +1,110 @@ +'use strict'; + +const TABLE = 'P2pTransactions'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable(TABLE, { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + transaction_id: { + type: Sequelize.UUID, + allowNull: false, + }, + deal_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'P2pDeals', + key: 'id' + } + }, + merchant_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + user_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + locked_asset_id: { + type: Sequelize.INTEGER, + allowNull: false, + }, + amount_digital_currency: { + type: Sequelize.DOUBLE, + allowNull: false, + }, + amount_fiat: { + type: Sequelize.DOUBLE, + allowNull: false, + }, + price: { + type: Sequelize.DOUBLE, + allowNull: false, + }, + payment_method_used: { + type: Sequelize.JSONB, + allowNull: true, + }, + user_status: { + type: Sequelize.ENUM('pending', 'confirmed', 'cancelled', 'appeal'), + allowNull: false, + }, + merchant_status: { + type: Sequelize.ENUM('pending', 'confirmed', 'cancelled', 'appeal'), + allowNull: false, + }, + cancellation_reason: { + type: Sequelize.STRING, + allowNull: true, + }, + settled_date: { + type: Sequelize.DATE, + allowNull: true, + }, + transaction_duration: { + type: Sequelize.INTEGER, + allowNull: true, + }, + transaction_status: { + type: Sequelize.ENUM('active', 'cancelled', 'complete', 'appealed', 'expired'), + allowNull: false, + }, + messages: { + type: Sequelize.JSONB, + allowNull: false, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }, + { + timestamps: true, + underscored: true + }); + }, + down: (queryInterface) => queryInterface.dropTable(TABLE), +}; diff --git a/server/db/migrations/20240215163268-create-p2p-dispute.js b/server/db/migrations/20240215163268-create-p2p-dispute.js new file mode 100644 index 0000000000..ce00356d6d --- /dev/null +++ b/server/db/migrations/20240215163268-create-p2p-dispute.js @@ -0,0 +1,71 @@ +'use strict'; + +const TABLE = 'P2pDisputes'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable(TABLE, { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + transaction_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'P2pTransactions', + key: 'id' + } + }, + initiator_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + defendant_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + reason: { + type: Sequelize.TEXT, + allowNull: false, + }, + resolution: { + type: Sequelize.TEXT, + allowNull: true, + }, + status: { + type: Sequelize.BOOLEAN, + allowNull: false, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }, + { + timestamps: true, + underscored: true + } + ); + }, + down: (queryInterface) => queryInterface.dropTable(TABLE), +}; diff --git a/server/db/migrations/20240215163270-create-p2p-merchant.js b/server/db/migrations/20240215163270-create-p2p-merchant.js new file mode 100644 index 0000000000..4c7d78f652 --- /dev/null +++ b/server/db/migrations/20240215163270-create-p2p-merchant.js @@ -0,0 +1,45 @@ +'use strict'; + +const TABLE = 'P2pMerchants'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable(TABLE, { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + user_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + blocked_users: { + type: Sequelize.ARRAY(Sequelize.INTEGER), + allowNull: true, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }, + { + timestamps: true, + underscored: true + } + ); + }, + down: (queryInterface) => queryInterface.dropTable(TABLE), +}; diff --git a/server/db/migrations/20240215163271-create-p2p-merchant-feedback.js b/server/db/migrations/20240215163271-create-p2p-merchant-feedback.js new file mode 100644 index 0000000000..4224ee96ba --- /dev/null +++ b/server/db/migrations/20240215163271-create-p2p-merchant-feedback.js @@ -0,0 +1,67 @@ +'use strict'; + +const TABLE = 'P2pMerchantsFeedback'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.createTable(TABLE, { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + merchant_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + user_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + transaction_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'P2pTransactions', + key: 'id' + } + }, + rating: { + type: Sequelize.INTEGER, + allowNull: false, + }, + comment: { + type: Sequelize.STRING, + allowNull: false, + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()'), + }, + }, + { + timestamps: true, + underscored: true + } + ); + }, + down: (queryInterface) => queryInterface.dropTable(TABLE), +}; diff --git a/server/db/migrations/20240428164315-create-referral-code.js b/server/db/migrations/20240428164315-create-referral-code.js new file mode 100644 index 0000000000..d75f4a4d2e --- /dev/null +++ b/server/db/migrations/20240428164315-create-referral-code.js @@ -0,0 +1,56 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable('ReferralCodes', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + user_id: { + type: Sequelize.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + code: { + type: Sequelize.STRING, + allowNull: false, + unique: true + }, + earning_rate: { + type: Sequelize.DOUBLE, + allowNull: false + }, + discount: { + type: Sequelize.DOUBLE, + allowNull: false, + defaultValue: 0 + }, + referral_count: { + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 0 + }, + created_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()') + }, + updated_at: { + allowNull: false, + type: Sequelize.DATE, + defaultValue: Sequelize.literal('NOW()') + } + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('ReferralCodes'); + }, +}; \ No newline at end of file diff --git a/server/db/migrations/20240501250127-add-affiliation-earning-rate.js b/server/db/migrations/20240501250127-add-affiliation-earning-rate.js new file mode 100644 index 0000000000..f6745fb73b --- /dev/null +++ b/server/db/migrations/20240501250127-add-affiliation-earning-rate.js @@ -0,0 +1,14 @@ +'use strict'; + +const TABLE = 'Affiliations'; +const COLUMN = 'earning_rate'; + +module.exports = { + up: (queryInterface, Sequelize) => + queryInterface.addColumn(TABLE, COLUMN, { + type: Sequelize.DOUBLE, + allowNull: true + }), + down: (queryInterface) => + queryInterface.removeColumn(TABLE, COLUMN) +}; \ No newline at end of file diff --git a/server/db/migrations/20240601125897-add-p2p-templates.js b/server/db/migrations/20240601125897-add-p2p-templates.js new file mode 100644 index 0000000000..7ba7538ee3 --- /dev/null +++ b/server/db/migrations/20240601125897-add-p2p-templates.js @@ -0,0 +1,171 @@ +'use strict'; +const TABLE = 'Status'; + +const templetes = [ + 'P2P_MERCHANT_IN_PROGRESS', + 'P2P_BUYER_PAID_ORDER', + 'P2P_ORDER_EXPIRED', + 'P2P_BUYER_CANCELLED_ORDER', + 'P2P_BUYER_APPEALED_ORDER', + 'P2P_VENDOR_CONFIRMED_ORDER', + 'P2P_VENDOR_CANCELLED_ORDER', + 'P2P_VENDOR_APPEALED_ORDER' +] + +const languages = { + 'P2P_MERCHANT_IN_PROGRESS': { + 'en': require('../../mail/strings/en.json').en.P2P_MERCHANT_IN_PROGRESS, + 'ar': require('../../mail/strings/ar.json').ar.P2P_MERCHANT_IN_PROGRESS, + 'de': require('../../mail/strings/de.json').de.P2P_MERCHANT_IN_PROGRESS, + 'es': require('../../mail/strings/es.json').es.P2P_MERCHANT_IN_PROGRESS, + 'fa': require('../../mail/strings/fa.json').fa.P2P_MERCHANT_IN_PROGRESS, + 'fr': require('../../mail/strings/fr.json').fr.P2P_MERCHANT_IN_PROGRESS, + 'id': require('../../mail/strings/id.json').id.P2P_MERCHANT_IN_PROGRESS, + 'ja': require('../../mail/strings/ja.json').ja.P2P_MERCHANT_IN_PROGRESS, + 'ko': require('../../mail/strings/ko.json').ko.P2P_MERCHANT_IN_PROGRESS, + 'pt': require('../../mail/strings/pt.json').pt.P2P_MERCHANT_IN_PROGRESS, + 'vi': require('../../mail/strings/vi.json').vi.P2P_MERCHANT_IN_PROGRESS, + 'zh': require('../../mail/strings/zh.json').zh.P2P_MERCHANT_IN_PROGRESS, + }, + 'P2P_BUYER_PAID_ORDER': { + 'en': require('../../mail/strings/en.json').en.P2P_BUYER_PAID_ORDER, + 'ar': require('../../mail/strings/ar.json').ar.P2P_BUYER_PAID_ORDER, + 'de': require('../../mail/strings/de.json').de.P2P_BUYER_PAID_ORDER, + 'es': require('../../mail/strings/es.json').es.P2P_BUYER_PAID_ORDER, + 'fa': require('../../mail/strings/fa.json').fa.P2P_BUYER_PAID_ORDER, + 'fr': require('../../mail/strings/fr.json').fr.P2P_BUYER_PAID_ORDER, + 'id': require('../../mail/strings/id.json').id.P2P_BUYER_PAID_ORDER, + 'ja': require('../../mail/strings/ja.json').ja.P2P_BUYER_PAID_ORDER, + 'ko': require('../../mail/strings/ko.json').ko.P2P_BUYER_PAID_ORDER, + 'pt': require('../../mail/strings/pt.json').pt.P2P_BUYER_PAID_ORDER, + 'vi': require('../../mail/strings/vi.json').vi.P2P_BUYER_PAID_ORDER, + 'zh': require('../../mail/strings/zh.json').zh.P2P_BUYER_PAID_ORDER, + }, + 'P2P_ORDER_EXPIRED': { + 'en': require('../../mail/strings/en.json').en.P2P_ORDER_EXPIRED, + 'ar': require('../../mail/strings/ar.json').ar.P2P_ORDER_EXPIRED, + 'de': require('../../mail/strings/de.json').de.P2P_ORDER_EXPIRED, + 'es': require('../../mail/strings/es.json').es.P2P_ORDER_EXPIRED, + 'fa': require('../../mail/strings/fa.json').fa.P2P_ORDER_EXPIRED, + 'fr': require('../../mail/strings/fr.json').fr.P2P_ORDER_EXPIRED, + 'id': require('../../mail/strings/id.json').id.P2P_ORDER_EXPIRED, + 'ja': require('../../mail/strings/ja.json').ja.P2P_ORDER_EXPIRED, + 'ko': require('../../mail/strings/ko.json').ko.P2P_ORDER_EXPIRED, + 'pt': require('../../mail/strings/pt.json').pt.P2P_ORDER_EXPIRED, + 'vi': require('../../mail/strings/vi.json').vi.P2P_ORDER_EXPIRED, + 'zh': require('../../mail/strings/zh.json').zh.P2P_ORDER_EXPIRED, + }, + 'P2P_BUYER_CANCELLED_ORDER': { + 'en': require('../../mail/strings/en.json').en.P2P_BUYER_CANCELLED_ORDER, + 'ar': require('../../mail/strings/ar.json').ar.P2P_BUYER_CANCELLED_ORDER, + 'de': require('../../mail/strings/de.json').de.P2P_BUYER_CANCELLED_ORDER, + 'es': require('../../mail/strings/es.json').es.P2P_BUYER_CANCELLED_ORDER, + 'fa': require('../../mail/strings/fa.json').fa.P2P_BUYER_CANCELLED_ORDER, + 'fr': require('../../mail/strings/fr.json').fr.P2P_BUYER_CANCELLED_ORDER, + 'id': require('../../mail/strings/id.json').id.P2P_BUYER_CANCELLED_ORDER, + 'ja': require('../../mail/strings/ja.json').ja.P2P_BUYER_CANCELLED_ORDER, + 'ko': require('../../mail/strings/ko.json').ko.P2P_BUYER_CANCELLED_ORDER, + 'pt': require('../../mail/strings/pt.json').pt.P2P_BUYER_CANCELLED_ORDER, + 'vi': require('../../mail/strings/vi.json').vi.P2P_BUYER_CANCELLED_ORDER, + 'zh': require('../../mail/strings/zh.json').zh.P2P_BUYER_CANCELLED_ORDER, + }, + 'P2P_BUYER_APPEALED_ORDER': { + 'en': require('../../mail/strings/en.json').en.P2P_BUYER_APPEALED_ORDER, + 'ar': require('../../mail/strings/ar.json').ar.P2P_BUYER_APPEALED_ORDER, + 'de': require('../../mail/strings/de.json').de.P2P_BUYER_APPEALED_ORDER, + 'es': require('../../mail/strings/es.json').es.P2P_BUYER_APPEALED_ORDER, + 'fa': require('../../mail/strings/fa.json').fa.P2P_BUYER_APPEALED_ORDER, + 'fr': require('../../mail/strings/fr.json').fr.P2P_BUYER_APPEALED_ORDER, + 'id': require('../../mail/strings/id.json').id.P2P_BUYER_APPEALED_ORDER, + 'ja': require('../../mail/strings/ja.json').ja.P2P_BUYER_APPEALED_ORDER, + 'ko': require('../../mail/strings/ko.json').ko.P2P_BUYER_APPEALED_ORDER, + 'pt': require('../../mail/strings/pt.json').pt.P2P_BUYER_APPEALED_ORDER, + 'vi': require('../../mail/strings/vi.json').vi.P2P_BUYER_APPEALED_ORDER, + 'zh': require('../../mail/strings/zh.json').zh.P2P_BUYER_APPEALED_ORDER, + }, + 'P2P_VENDOR_CONFIRMED_ORDER': { + 'en': require('../../mail/strings/en.json').en.P2P_VENDOR_CONFIRMED_ORDER, + 'ar': require('../../mail/strings/ar.json').ar.P2P_VENDOR_CONFIRMED_ORDER, + 'de': require('../../mail/strings/de.json').de.P2P_VENDOR_CONFIRMED_ORDER, + 'es': require('../../mail/strings/es.json').es.P2P_VENDOR_CONFIRMED_ORDER, + 'fa': require('../../mail/strings/fa.json').fa.P2P_VENDOR_CONFIRMED_ORDER, + 'fr': require('../../mail/strings/fr.json').fr.P2P_VENDOR_CONFIRMED_ORDER, + 'id': require('../../mail/strings/id.json').id.P2P_VENDOR_CONFIRMED_ORDER, + 'ja': require('../../mail/strings/ja.json').ja.P2P_VENDOR_CONFIRMED_ORDER, + 'ko': require('../../mail/strings/ko.json').ko.P2P_VENDOR_CONFIRMED_ORDER, + 'pt': require('../../mail/strings/pt.json').pt.P2P_VENDOR_CONFIRMED_ORDER, + 'vi': require('../../mail/strings/vi.json').vi.P2P_VENDOR_CONFIRMED_ORDER, + 'zh': require('../../mail/strings/zh.json').zh.P2P_VENDOR_CONFIRMED_ORDER, + }, + 'P2P_VENDOR_CANCELLED_ORDER': { + 'en': require('../../mail/strings/en.json').en.P2P_VENDOR_CANCELLED_ORDER, + 'ar': require('../../mail/strings/ar.json').ar.P2P_VENDOR_CANCELLED_ORDER, + 'de': require('../../mail/strings/de.json').de.P2P_VENDOR_CANCELLED_ORDER, + 'es': require('../../mail/strings/es.json').es.P2P_VENDOR_CANCELLED_ORDER, + 'fa': require('../../mail/strings/fa.json').fa.P2P_VENDOR_CANCELLED_ORDER, + 'fr': require('../../mail/strings/fr.json').fr.P2P_VENDOR_CANCELLED_ORDER, + 'id': require('../../mail/strings/id.json').id.P2P_VENDOR_CANCELLED_ORDER, + 'ja': require('../../mail/strings/ja.json').ja.P2P_VENDOR_CANCELLED_ORDER, + 'ko': require('../../mail/strings/ko.json').ko.P2P_VENDOR_CANCELLED_ORDER, + 'pt': require('../../mail/strings/pt.json').pt.P2P_VENDOR_CANCELLED_ORDER, + 'vi': require('../../mail/strings/vi.json').vi.P2P_VENDOR_CANCELLED_ORDER, + 'zh': require('../../mail/strings/zh.json').zh.P2P_VENDOR_CANCELLED_ORDER, + }, + 'P2P_VENDOR_APPEALED_ORDER': { + 'en': require('../../mail/strings/en.json').en.P2P_VENDOR_APPEALED_ORDER, + 'ar': require('../../mail/strings/ar.json').ar.P2P_VENDOR_APPEALED_ORDER, + 'de': require('../../mail/strings/de.json').de.P2P_VENDOR_APPEALED_ORDER, + 'es': require('../../mail/strings/es.json').es.P2P_VENDOR_APPEALED_ORDER, + 'fa': require('../../mail/strings/fa.json').fa.P2P_VENDOR_APPEALED_ORDER, + 'fr': require('../../mail/strings/fr.json').fr.P2P_VENDOR_APPEALED_ORDER, + 'id': require('../../mail/strings/id.json').id.P2P_VENDOR_APPEALED_ORDER, + 'ja': require('../../mail/strings/ja.json').ja.P2P_VENDOR_APPEALED_ORDER, + 'ko': require('../../mail/strings/ko.json').ko.P2P_VENDOR_APPEALED_ORDER, + 'pt': require('../../mail/strings/pt.json').pt.P2P_VENDOR_APPEALED_ORDER, + 'vi': require('../../mail/strings/vi.json').vi.P2P_VENDOR_APPEALED_ORDER, + 'zh': require('../../mail/strings/zh.json').zh.P2P_VENDOR_APPEALED_ORDER, + } +}; + +const models = require('../models'); + + +module.exports = { + async up(queryInterface) { + + for (const templete of templetes) { + const statusModel = models[TABLE]; + const status = await statusModel.findOne({}); + + if(!status?.email) return; + const emailTemplates = { + ...status.email, + }; + + let hasTemplate = true; + for (const [language, emailTemplate] of Object.entries(languages[templete])) { + + if (status.email && status.email[language] && !status.email[language].hasOwnProperty(templete)) { + hasTemplate = false; + emailTemplates[language] = { + ...status.email[language], + [templete]: emailTemplate + }; + } + } + + if (!hasTemplate) { + await statusModel.update( + { email: emailTemplates }, + { where: { id: status.id } } + ); + } + } + }, + + down: () => { + return new Promise((resolve) => { + resolve(); + }); + } +}; diff --git a/server/db/migrations/20240610126432-add-p2p-order-close-templates.js b/server/db/migrations/20240610126432-add-p2p-order-close-templates.js new file mode 100644 index 0000000000..4fe2c9e0e0 --- /dev/null +++ b/server/db/migrations/20240610126432-add-p2p-order-close-templates.js @@ -0,0 +1,65 @@ +'use strict'; +const TABLE = 'Status'; + +const templetes = [ + 'P2P_ORDER_CLOSED' +] + +const languages = { + 'P2P_ORDER_CLOSED': { + 'en': require('../../mail/strings/en.json').en.P2P_ORDER_CLOSED, + 'ar': require('../../mail/strings/ar.json').ar.P2P_ORDER_CLOSED, + 'de': require('../../mail/strings/de.json').de.P2P_ORDER_CLOSED, + 'es': require('../../mail/strings/es.json').es.P2P_ORDER_CLOSED, + 'fa': require('../../mail/strings/fa.json').fa.P2P_ORDER_CLOSED, + 'fr': require('../../mail/strings/fr.json').fr.P2P_ORDER_CLOSED, + 'id': require('../../mail/strings/id.json').id.P2P_ORDER_CLOSED, + 'ja': require('../../mail/strings/ja.json').ja.P2P_ORDER_CLOSED, + 'ko': require('../../mail/strings/ko.json').ko.P2P_ORDER_CLOSED, + 'pt': require('../../mail/strings/pt.json').pt.P2P_ORDER_CLOSED, + 'vi': require('../../mail/strings/vi.json').vi.P2P_ORDER_CLOSED, + 'zh': require('../../mail/strings/zh.json').zh.P2P_ORDER_CLOSED, + }, +}; + +const models = require('../models'); + +module.exports = { + async up(queryInterface) { + + for (const templete of templetes) { + const statusModel = models[TABLE]; + const status = await statusModel.findOne({}); + + if(!status?.email) return; + const emailTemplates = { + ...status.email, + }; + + let hasTemplate = true; + for (const [language, emailTemplate] of Object.entries(languages[templete])) { + + if (status.email && status.email[language] && !status.email[language].hasOwnProperty(templete)) { + hasTemplate = false; + emailTemplates[language] = { + ...status.email[language], + [templete]: emailTemplate + }; + } + } + + if (!hasTemplate) { + await statusModel.update( + { email: emailTemplates }, + { where: { id: status.id } } + ); + } + } + }, + + down: () => { + return new Promise((resolve) => { + resolve(); + }); + } +}; diff --git a/server/db/migrations/20240610126439-add-p2p-transaction-closed-enum.js b/server/db/migrations/20240610126439-add-p2p-transaction-closed-enum.js new file mode 100644 index 0000000000..dab375742c --- /dev/null +++ b/server/db/migrations/20240610126439-add-p2p-transaction-closed-enum.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + try { + await queryInterface.sequelize.query(` + ALTER TYPE "enum_P2pTransactions_transaction_status" + ADD VALUE 'closed'; + `); + } + catch (error) { + return error; + } + }, + down: () => + new Promise((resolve) => { + resolve(); + }) +}; \ No newline at end of file diff --git a/server/db/models/affiliation.js b/server/db/models/affiliation.js index 34dafac896..48bdeb89df 100644 --- a/server/db/models/affiliation.js +++ b/server/db/models/affiliation.js @@ -26,9 +26,14 @@ module.exports = function (sequelize, DataTypes) { key: 'id' } }, + earning_rate: { + type: DataTypes.DOUBLE, + allowNull: true + }, code: { type: DataTypes.STRING, - allowNull: true + allowNull: true, + unique: true } }, { diff --git a/server/db/models/index.js b/server/db/models/index.js index ebb34a2f2c..e1f7f38c10 100644 --- a/server/db/models/index.js +++ b/server/db/models/index.js @@ -52,6 +52,20 @@ model = require(path.join(__dirname, './transactionLimit'))(sequelize, Sequelize db[model.name] = model; model = require(path.join(__dirname, './balanceHistory'))(sequelize, Sequelize.DataTypes); db[model.name] = model; +model = require(path.join(__dirname, './p2pTransaction'))(sequelize, Sequelize.DataTypes); +db[model.name] = model; +model = require(path.join(__dirname, './p2pDeal'))(sequelize, Sequelize.DataTypes); +db[model.name] = model; +model = require(path.join(__dirname, './p2pDispute'))(sequelize, Sequelize.DataTypes); +db[model.name] = model; +model = require(path.join(__dirname, './p2pMerchant'))(sequelize, Sequelize.DataTypes); +db[model.name] = model; +model = require(path.join(__dirname, './p2pMerchantFeedback'))(sequelize, Sequelize.DataTypes); +db[model.name] = model; +model = require(path.join(__dirname, './referralHistory'))(sequelize, Sequelize.DataTypes); +db[model.name] = model; +model = require(path.join(__dirname, './referralCode'))(sequelize, Sequelize.DataTypes); +db[model.name] = model; Object.keys(db).forEach(function (modelName) { if ('associate' in db[modelName]) { @@ -62,4 +76,4 @@ Object.keys(db).forEach(function (modelName) { db.sequelize = sequelize; db.Sequelize = Sequelize; -module.exports = db; +module.exports = db; \ No newline at end of file diff --git a/server/db/models/p2pDeal.js b/server/db/models/p2pDeal.js new file mode 100644 index 0000000000..69e6b3faf8 --- /dev/null +++ b/server/db/models/p2pDeal.js @@ -0,0 +1,101 @@ +'use strict'; + +module.exports = function (sequelize, DataTypes) { + const Deal = sequelize.define( + 'P2pDeal', + { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER + }, + merchant_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + side: { + type: DataTypes.STRING, + allowNull: false, + }, + price_type: { + type: DataTypes.ENUM('static', 'dynamic'), + allowNull: false, + }, + buying_asset: { + type: DataTypes.STRING, + allowNull: false, + }, + spending_asset: { + type: DataTypes.STRING, + allowNull: false, + }, + exchange_rate: { + type: DataTypes.DOUBLE, + allowNull: true, + }, + spread: { + type: DataTypes.DOUBLE, + allowNull: true, + }, + total_order_amount: { + type: DataTypes.DOUBLE, + allowNull: false, + }, + min_order_value: { + type: DataTypes.DOUBLE, + allowNull: false, + }, + max_order_value: { + type: DataTypes.DOUBLE, + allowNull: false, + }, + terms: { + type: DataTypes.TEXT, + allowNull: true, + }, + auto_response: { + type: DataTypes.STRING, + allowNull: true, + }, + region: { + type: DataTypes.STRING, + allowNull: true, + }, + payment_methods: { + type: DataTypes.JSONB, + allowNull: false, + }, + status: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + }, + { + timestamps: true, + underscored: true, + tableName: 'P2pDeals', + } + ); + + + Deal.associate = (models) => { + Deal.belongsTo(models.User, { + as: 'merchant', + foreignKey: 'merchant_id', + targetKey: 'id', + onDelete: 'CASCADE' + }); + + Deal.hasMany(models.P2pTransaction, { + foreignKey: 'deal_id' + }); + }; + + return Deal; +}; diff --git a/server/db/models/p2pDispute.js b/server/db/models/p2pDispute.js new file mode 100644 index 0000000000..be4e1edfff --- /dev/null +++ b/server/db/models/p2pDispute.js @@ -0,0 +1,55 @@ +'use strict'; + +module.exports = function (sequelize, DataTypes) { + const Dispute = sequelize.define( + 'P2pDispute', + { + transaction_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'p2pTransactions', + key: 'id' + } + }, + initiator_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + defendant_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + reason: { + type: DataTypes.TEXT, + allowNull: false, + }, + resolution: { + type: DataTypes.TEXT, + allowNull: true, + }, + status: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + }, + { + timestamps: true, + underscored: true, + tableName: 'P2pDisputes', + } + ); + + return Dispute; +}; diff --git a/server/db/models/p2pMerchant.js b/server/db/models/p2pMerchant.js new file mode 100644 index 0000000000..08f61adfc8 --- /dev/null +++ b/server/db/models/p2pMerchant.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = function (sequelize, DataTypes) { + const Merchant = sequelize.define( + 'P2pMerchant', + { + user_id: { + type: DataTypes.INTEGER, + allowNull: false, + primaryKey: true, + }, + blocked_users: { + type: DataTypes.ARRAY(DataTypes.INTEGER), + allowNull: true, + }, + // ADD OTHER STUFF + }, + { + timestamps: false, + underscored: true, + tableName: 'P2pMerchants', + } + ); + + return Merchant; +}; diff --git a/server/db/models/p2pMerchantFeedback.js b/server/db/models/p2pMerchantFeedback.js new file mode 100644 index 0000000000..e6d79d517c --- /dev/null +++ b/server/db/models/p2pMerchantFeedback.js @@ -0,0 +1,72 @@ +'use strict'; + +module.exports = function (sequelize, DataTypes) { + const MerchantsFeedback = sequelize.define( + 'P2pMerchantsFeedback', + { + transaction_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'p2pTransactions', + key: 'id' + } + }, + merchant_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + user_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + rating: { + type: DataTypes.INTEGER, + allowNull: false, + }, + comment: { + type: DataTypes.STRING, + allowNull: true, + }, + }, + { + timestamps: true, + underscored: true, + tableName: 'P2pMerchantsFeedback', + } + ); + + MerchantsFeedback.associate = (models) => { + MerchantsFeedback.belongsTo(models.User, { + as: 'merchant', + foreignKey: 'merchant_id', + targetKey: 'id', + onDelete: 'CASCADE' + }); + MerchantsFeedback.belongsTo(models.User, { + as: 'user', + foreignKey: 'user_id', + targetKey: 'id', + onDelete: 'CASCADE' + }); + MerchantsFeedback.belongsTo(models.P2pTransaction, { + as: 'transaction', + foreignKey: 'transaction_id', + targetKey: 'id', + onDelete: 'CASCADE' + }); + }; + + return MerchantsFeedback; +}; diff --git a/server/db/models/p2pTransaction.js b/server/db/models/p2pTransaction.js new file mode 100644 index 0000000000..e1c156633d --- /dev/null +++ b/server/db/models/p2pTransaction.js @@ -0,0 +1,124 @@ +'use strict'; + +module.exports = function (sequelize, DataTypes) { + const Transaction = sequelize.define( + 'P2pTransaction', + { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER + }, + transaction_id: { + type: DataTypes.UUID, + allowNull: false, + }, + deal_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'p2pDeals', + key: 'id' + } + }, + merchant_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + user_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + locked_asset_id: { + type: DataTypes.INTEGER, + allowNull: false, + }, + amount_digital_currency: { + type: DataTypes.DOUBLE, + allowNull: false, + }, + amount_fiat: { + type: DataTypes.DOUBLE, + allowNull: false, + }, + price: { + type: DataTypes.DOUBLE, + allowNull: false, + }, + payment_method_used: { + type: DataTypes.JSONB, + allowNull: true, + }, + user_status: { + type: DataTypes.ENUM('pending', 'confirmed', 'cancelled', 'appeal'), + allowNull: false, + }, + merchant_status: { + type: DataTypes.ENUM('pending', 'confirmed', 'cancelled', 'appeal'), + allowNull: false, + }, + cancellation_reason: { + type: DataTypes.STRING, + allowNull: true, + }, + settled_date: { + type: DataTypes.DATE, + allowNull: true, + }, + transaction_duration: { + type: DataTypes.INTEGER, + allowNull: true, + }, + transaction_status: { + type: DataTypes.ENUM('active', 'cancelled', 'complete', 'appealed', 'expired', 'closed'), + allowNull: false, + }, + messages: { + type: DataTypes.JSONB, + allowNull: false, + }, + }, + { + timestamps: true, + underscored: true, + tableName: 'P2pTransactions', + } + ); + + Transaction.associate = (models) => { + Transaction.belongsTo(models.User, { + as: 'merchant', + foreignKey: 'merchant_id', + targetKey: 'id', + onDelete: 'CASCADE' + }); + + Transaction.belongsTo(models.User, { + as: 'buyer', + foreignKey: 'user_id', + targetKey: 'id', + onDelete: 'CASCADE' + }); + + Transaction.belongsTo(models.P2pDeal, { + as: 'deal', + foreignKey: 'deal_id', + targetKey: 'id', + onDelete: 'CASCADE' + }); + }; + + return Transaction; +}; diff --git a/server/db/models/referralCode.js b/server/db/models/referralCode.js new file mode 100644 index 0000000000..77afeea021 --- /dev/null +++ b/server/db/models/referralCode.js @@ -0,0 +1,51 @@ +'use strict'; + + +module.exports = function (sequelize, DataTypes) { + const ReferralCode = sequelize.define('ReferralCode', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER + }, + user_id: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + code: { + type: DataTypes.STRING, + allowNull: false, + unique: true + }, + earning_rate: { + type: DataTypes.DOUBLE, + allowNull: false + }, + discount: { + type: DataTypes.DOUBLE, + allowNull: false, + defaultValue: 0 + }, + referral_count: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + } + }, { + timestamps: true, + underscored: true, + tableName: 'ReferralCodes' + }); + + ReferralCode.associate = (models) => { + + }; + + return ReferralCode; +}; \ No newline at end of file diff --git a/server/db/models/referralHistory.js b/server/db/models/referralHistory.js new file mode 100644 index 0000000000..ea686cc236 --- /dev/null +++ b/server/db/models/referralHistory.js @@ -0,0 +1,61 @@ +'use strict'; + + +module.exports = function (sequelize, DataTypes) { + const ReferralHistory = sequelize.define('ReferralHistory', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER + }, + referer: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + referee: { + type: DataTypes.INTEGER, + onDelete: 'CASCADE', + allowNull: false, + references: { + model: 'Users', + key: 'id' + } + }, + code: { + type: DataTypes.STRING, + allowNull: true, + }, + coin: { + type: DataTypes.STRING, + allowNull: false + }, + accumulated_fees: { + type: DataTypes.DOUBLE, + allowNull: false + }, + last_settled: { + type: DataTypes.DATE, + allowNull: false + }, + status: { + type: DataTypes.BOOLEAN, + allowNull: false + } + }, { + timestamps: true, + underscored: true, + tableName: 'ReferralHistories' + }); + + ReferralHistory.associate = (models) => { + + }; + + return ReferralHistory; +}; \ No newline at end of file diff --git a/server/init.js b/server/init.js index 218d2c2f8d..d63d305ef4 100644 --- a/server/init.js +++ b/server/init.js @@ -142,7 +142,7 @@ const checkStatus = () => { configuration.coins[coin.symbol] = { ...coin, ...configuration?.kit?.fiat_fees?.[coin.symbol] - } + }; } else { configuration.coins[coin.symbol] = coin; } @@ -162,12 +162,12 @@ const checkStatus = () => { // only add the network pair if both coins in the market are already subscribed in the exchange const [ base, quote ] = e.split('-'); if (configuration.coins[base] && configuration.coins[quote]) { - configuration.networkQuickTrades.push(exchange.brokers[e]) + configuration.networkQuickTrades.push(exchange.brokers[e]); return e; } }); - let quickTradePairs = quickTrades.map((q) => q.symbol) + let quickTradePairs = quickTrades.map((q) => q.symbol); // check the status of quickTrades for (let qt of quickTrades) { @@ -220,8 +220,8 @@ const checkStatus = () => { symbol: qt.symbol, active: qt.active }; - configuration.quicktrade.push(item) - }) + configuration.quicktrade.push(item); + }); for (let tier of tiers) { @@ -241,7 +241,7 @@ const checkStatus = () => { }; const defaultFees = DEFAULT_FEES[exchange.plan] ? DEFAULT_FEES[exchange.plan] - : { maker: 0.2, taker: 0.2 } + : { maker: 0.2, taker: 0.2 }; for (let pair of quickTradePairs) { if (!isNumber(tier.fees.maker[pair])) { @@ -328,6 +328,12 @@ const checkStatus = () => { ); loggerInit.info('init/checkStatus/activation complete'); return networkNodeLib; + }) + .catch((err) => { + loggerInit.info('init/checkStatus/catch error', err.message); + setTimeout(() => { + process.exit(0); + }, 5000); }); }; diff --git a/server/mail/index.js b/server/mail/index.js index 957aa776da..21f1db819e 100644 --- a/server/mail/index.js +++ b/server/mail/index.js @@ -63,6 +63,14 @@ const sendEmail = ( case MAILTYPE.USER_DELETED: case MAILTYPE.DEPOSIT: case MAILTYPE.WITHDRAWAL: + case MAILTYPE.P2P_MERCHANT_IN_PROGRESS: + case MAILTYPE.P2P_BUYER_PAID_ORDER: + case MAILTYPE.P2P_ORDER_EXPIRED: + case MAILTYPE.P2P_BUYER_CANCELLED_ORDER: + case MAILTYPE.P2P_BUYER_APPEALED_ORDER: + case MAILTYPE.P2P_VENDOR_CONFIRMED_ORDER: + case MAILTYPE.P2P_VENDOR_CANCELLED_ORDER: + case MAILTYPE.P2P_VENDOR_APPEALED_ORDER: case MAILTYPE.DOC_REJECTED: case MAILTYPE.DOC_VERIFIED: { to.BccAddresses = BCC_ADDRESSES(); diff --git a/server/mail/strings/ar.json b/server/mail/strings/ar.json index 86f24ab71b..14708b0b6c 100644 --- a/server/mail/strings/ar.json +++ b/server/mail/strings/ar.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

عزيزي/عزيزتي ${name}

لقد سجلنا أن المصادقة ذات العاملين (2FA) تم تمكينها على حسابك.

الوقت: ${time}
البلد: ${country}
عنوان IP: ${ip}

بكل تقدير
فريق ${api_name}

", "title": "تم تمكين 2FA" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

عزيزي ${name}

لقد اكتشفنا نشاطًا في صفقتك P2P، مما قد يشير إلى طلب عميل للتعامل معك. للتحقق من حالة صفقتك، يرجى النقر على الرابط أدناه:

شكرًا لاستخدامك منصتنا.

تم تقديم الطلب من: ${ip}

تحياتنا
فريق ${api_name}

", + "title": "تحديث حول صفقتك P2P" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

عزيزي ${name}

لقد قمت بتحديد طلبك على أنه مدفوع. بانتظار البائع للتحقق، التأكيد وإطلاق الأموال

شكرًا لاستخدامك منصتنا.

تم تقديم الطلب من: ${ip}

تحياتنا
فريق ${api_name}

", + "title": "تم اكتشاف نشاط في طلبك P2P" + }, + "P2P_ORDER_EXPIRED": { + "html": "

عزيزي ${name}

انتهت صلاحية طلبك بسبب عدم النشاط في الوقت المحدد.

شكرًا لاستخدامك منصتنا.

تم تقديم الطلب من: ${ip}

تحياتنا
فريق ${api_name}

", + "title": "تم اكتشاف نشاط في طلبك P2P" + }, + "P2P_ORDER_CLOSED": { + "html": "

عزيزي ${name}

تم إغلاق طلبك بواسطة فريق الدعم بعد الوصول إلى قرار بشأن طلب الاستئناف

شكرًا لاستخدامك منصتنا.

مع تحيات
فريق ${api_name}

", + "title": "تم اكتشاف نشاط في طلبك P2P" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

عزيزي ${name}

لقد قمت بإلغاء طلبك. تم إغلاق الصفقة.

شكرًا لاستخدامك منصتنا.

تم تقديم الطلب من: ${ip}

تحياتنا
فريق ${api_name}

", + "title": "تم اكتشاف نشاط في طلبك P2P" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

عزيزي ${name}

لقد قمت بالاستئناف على طلبك. يرجى الاتصال بالدعم لحل المشكلة.

تم تقديم الطلب من: ${ip}

تحياتنا
فريق ${api_name}

", + "title": "تم اكتشاف نشاط في طلبك P2P" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

عزيزي ${name}

لقد قمت بتأكيد الصفقة وإطلاق الأموال.

شكرًا لاستخدامك منصتنا.

تم تقديم الطلب من: ${ip}

تحياتنا
فريق ${api_name}

", + "title": "تم اكتشاف نشاط في طلبك P2P" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

عزيزي ${name}

لقد قمت بإلغاء طلبك. تم إغلاق الصفقة.

شكرًا لاستخدامك منصتنا.

تم تقديم الطلب من: ${ip}

تحياتنا
فريق ${api_name}

", + "title": "تم اكتشاف نشاط في طلبك P2P" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

عزيزي ${name}

لقد قمت بالاستئناف على طلبك. يرجى الاتصال بالدعم لحل المشكلة.

شكرًا لاستخدامك منصتنا.

تم تقديم الطلب من: ${ip}

تحياتنا
فريق ${api_name}

", + "title": "تم اكتشاف نشاط في طلبك P2P" } } } \ No newline at end of file diff --git a/server/mail/strings/de.json b/server/mail/strings/de.json index 6e1f4d08ad..60354cb7b3 100644 --- a/server/mail/strings/de.json +++ b/server/mail/strings/de.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

Lieber/Liebe ${name}

Wir haben festgestellt, dass Ihre Zwei-Faktor-Authentifizierung (2FA) aktiviert wurde auf Ihrem Konto.

Zeit: ${time}
Land: ${country}
IP-Adresse: ${ip}

Mit freundlichen Grüßen
Team ${api_name}

", "title": "OTP Aktiviert" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

Lieber ${name}

Wir haben Aktivitäten auf Ihrem P2P-Geschäft festgestellt, was darauf hinweisen könnte, dass ein Kunde eine Transaktion mit Ihnen anfordert. Um den Status Ihres Geschäfts zu überprüfen, klicken Sie bitte auf den untenstehenden Link:

Vielen Dank, dass Sie unsere Plattform nutzen.

Anfrage eingeleitet von: ${ip}

Mit freundlichen Grüßen
Ihr ${api_name} Team

", + "title": "Aktualisierung zu Ihrem P2P-Geschäft" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

Lieber ${name}

Sie haben Ihre Bestellung als bezahlt markiert. Warten auf den Verkäufer, um zu überprüfen, zu bestätigen und die Gelder freizugeben

Vielen Dank, dass Sie unsere Plattform nutzen.

Anfrage eingeleitet von: ${ip}

Mit freundlichen Grüßen
Ihr ${api_name} Team

", + "title": "Aktivität auf Ihrer P2P-Bestellung festgestellt" + }, + "P2P_ORDER_EXPIRED": { + "html": "

Lieber ${name}

Ihre Bestellung ist aufgrund von Inaktivität innerhalb der gegebenen Zeit abgelaufen.

Vielen Dank, dass Sie unsere Plattform nutzen.

Anfrage eingeleitet von: ${ip}

Mit freundlichen Grüßen
Ihr ${api_name} Team

", + "title": "Aktivität auf Ihrer P2P-Bestellung festgestellt" + }, + "P2P_ORDER_CLOSED": { + "html": "

Lieber ${name}

Ihre Bestellung wurde von unserem Support-Team geschlossen, nachdem eine Entscheidung über den Einspruchsantrag getroffen wurde

Vielen Dank, dass Sie unsere Plattform nutzen.

Mit freundlichen Grüßen
Ihr ${api_name} Team

", + "title": "Aktivität auf Ihrer P2P-Bestellung festgestellt" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

Lieber ${name}

Sie haben Ihre Bestellung storniert. Die Transaktion ist abgeschlossen.

Vielen Dank, dass Sie unsere Plattform nutzen.

Anfrage eingeleitet von: ${ip}

Mit freundlichen Grüßen
Ihr ${api_name} Team

", + "title": "Aktivität auf Ihrer P2P-Bestellung festgestellt" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

Lieber ${name}

Sie haben Ihre Bestellung angefochten. Bitte kontaktieren Sie den Support, um das Problem zu lösen.

Anfrage eingeleitet von: ${ip}

Mit freundlichen Grüßen
Ihr ${api_name} Team

", + "title": "Aktivität auf Ihrer P2P-Bestellung festgestellt" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

Lieber ${name}

Sie haben die Transaktion bestätigt und die Gelder freigegeben.

Vielen Dank, dass Sie unsere Plattform nutzen.

Anfrage eingeleitet von: ${ip}

Mit freundlichen Grüßen
Ihr ${api_name} Team

", + "title": "Aktivität auf Ihrer P2P-Bestellung festgestellt" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

Lieber ${name}

Sie haben Ihre Bestellung storniert. Die Transaktion ist abgeschlossen.

Vielen Dank, dass Sie unsere Plattform nutzen.

Anfrage eingeleitet von: ${ip}

Mit freundlichen Grüßen
Ihr ${api_name} Team

", + "title": "Aktivität auf Ihrer P2P-Bestellung festgestellt" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

Lieber ${name}

Sie haben Ihre Bestellung angefochten. Bitte kontaktieren Sie den Support, um das Problem zu lösen.

Vielen Dank, dass Sie unsere Plattform nutzen.

Anfrage eingeleitet von: ${ip}

Mit freundlichen Grüßen
Ihr ${api_name} Team

", + "title": "Aktivität auf Ihrer P2P-Bestellung festgestellt" } } } \ No newline at end of file diff --git a/server/mail/strings/en.json b/server/mail/strings/en.json index 0e8ecaa424..2ca334c27d 100644 --- a/server/mail/strings/en.json +++ b/server/mail/strings/en.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

Dear ${name}

We have recorded that your two-factor authentication (2FA) has been enabled on your account.

Time: ${time}
Country: ${country}
IP Address: ${ip}

Regards
${api_name} team

", "title": "OTP Enabled" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

Dear ${name}

We have detected activity on your P2P deal, which may indicate a customer requesting to transact with you. To check the status of your deal, please click the link below:

Thank you for using our platform.

Request initiated from: ${ip}

Regards
${api_name} team

", + "title": "Update on Your P2P Deal" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

Dear ${name}

You have marked your order as paid. Waiting for vendor to check, confirm and release funds

Thank you for using our platform.

Request initiated from: ${ip}

Regards
${api_name} team

", + "title": "Activity Detected on Your P2P Order" + }, + "P2P_ORDER_EXPIRED": { + "html": "

Dear ${name}

Your order expired due to inactivity in the given time.

Thank you for using our platform.

Request initiated from: ${ip}

Regards
${api_name} team

", + "title": "Activity Detected on Your P2P Order" + }, + "P2P_ORDER_CLOSED": { + "html": "

Dear ${name}

Your order has been closed by our support team after reaching a verdict on the appeal request

Thank you for using our platform.

Regards
${api_name} team

", + "title": "Activity Detected on Your P2P Order" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

Dear ${name}

You have cancelled your order. Transaction is closed.

Thank you for using our platform.

Request initiated from: ${ip}

Regards
${api_name} team

", + "title": "Activity Detected on Your P2P Order" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

Dear ${name}

You have appealed your order. Please contact support to resolve the issue.

Request initiated from: ${ip}

Regards
${api_name} team

", + "title": "Activity Detected on Your P2P Order" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

Dear ${name}

You have confirmed the transaction and released the funds.

Thank you for using our platform.

Request initiated from: ${ip}

Regards
${api_name} team

", + "title": "Activity Detected on Your P2P Order" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

Dear ${name}

You have cancelled your order. Transaction is closed.

Thank you for using our platform.

Request initiated from: ${ip}

Regards
${api_name} team

", + "title": "Activity Detected on Your P2P Order" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

Dear ${name}

You have appealed your order. Please contact support to resolve the issue.

Thank you for using our platform.

Request initiated from: ${ip}

Regards
${api_name} team

", + "title": "Activity Detected on Your P2P Order" } } } \ No newline at end of file diff --git a/server/mail/strings/es.json b/server/mail/strings/es.json index 476039ce3c..1cb77b5c3f 100644 --- a/server/mail/strings/es.json +++ b/server/mail/strings/es.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

Estimado/a ${name}

Hemos registrado que se ha activado la autenticación de dos factores (2FA) en su cuenta.

Hora: ${time}
País: ${country}
Dirección IP: ${ip}

Saludos
Equipo ${api_name}

", "title": "OTP Activada" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

Estimado ${name}

Hemos detectado actividad en su trato P2P, lo que puede indicar que un cliente está solicitando realizar una transacción con usted. Para verificar el estado de su trato, haga clic en el enlace a continuación:

Gracias por usar nuestra plataforma.

Solicitud iniciada desde: ${ip}

Saludos
equipo ${api_name}

", + "title": "Actualización sobre su trato P2P" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

Estimado ${name}

Ha marcado su pedido como pagado. Esperando a que el vendedor verifique, confirme y libere los fondos

Gracias por usar nuestra plataforma.

Solicitud iniciada desde: ${ip}

Saludos
equipo ${api_name}

", + "title": "Actividad detectada en su pedido P2P" + }, + "P2P_ORDER_EXPIRED": { + "html": "

Estimado ${name}

Su pedido ha expirado debido a la inactividad en el tiempo dado.

Gracias por usar nuestra plataforma.

Solicitud iniciada desde: ${ip}

Saludos
equipo ${api_name}

", + "title": "Actividad detectada en su pedido P2P" + }, + "P2P_ORDER_CLOSED": { + "html": "

Estimado ${name}

Su pedido ha sido cerrado por nuestro equipo de soporte después de llegar a un veredicto sobre la solicitud de apelación

Gracias por usar nuestra plataforma.

Saludos
equipo ${api_name}

", + "title": "Actividad detectada en su pedido P2P" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

Estimado ${name}

Ha cancelado su pedido. La transacción está cerrada.

Gracias por usar nuestra plataforma.

Solicitud iniciada desde: ${ip}

Saludos
equipo ${api_name}

", + "title": "Actividad detectada en su pedido P2P" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

Estimado ${name}

Ha apelado su pedido. Por favor, póngase en contacto con el soporte para resolver el problema.

Solicitud iniciada desde: ${ip}

Saludos
equipo ${api_name}

", + "title": "Actividad detectada en su pedido P2P" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

Estimado ${name}

Ha confirmado la transacción y liberado los fondos.

Gracias por usar nuestra plataforma.

Solicitud iniciada desde: ${ip}

Saludos
equipo ${api_name}

", + "title": "Actividad detectada en su pedido P2P" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

Estimado ${name}

Ha cancelado su pedido. La transacción está cerrada.

Gracias por usar nuestra plataforma.

Solicitud iniciada desde: ${ip}

Saludos
equipo ${api_name}

", + "title": "Actividad detectada en su pedido P2P" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

Estimado ${name}

Ha apelado su pedido. Por favor, póngase en contacto con el soporte para resolver el problema.

Gracias por usar nuestra plataforma.

Solicitud iniciada desde: ${ip}

Saludos
equipo ${api_name}

", + "title": "Actividad detectada en su pedido P2P" } } } \ No newline at end of file diff --git a/server/mail/strings/fa.json b/server/mail/strings/fa.json index c35b82651b..9566bea230 100644 --- a/server/mail/strings/fa.json +++ b/server/mail/strings/fa.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

عزیز ${name}

ما ثبت کرده‌ایم که احراز هویت دو عاملی (2FA) در حساب شما فعال شده است.

زمان: ${time}
کشور: ${country}
آدرس IP: ${ip}

با احترام
تیم ${api_name}

", "title": "احراز هویت دو عاملی فعال شد" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

عزیز ${name}

ما فعالیتی در معامله P2P شما شناسایی کرده‌ایم که ممکن است نشان‌دهنده درخواست مشتری برای معامله با شما باشد. برای بررسی وضعیت معامله خود، لطفاً روی لینک زیر کلیک کنید:

از اینکه از پلتفرم ما استفاده می‌کنید سپاسگزاریم.

درخواست از: ${ip}

با احترام
تیم ${api_name}

", + "title": "به‌روزرسانی در مورد معامله P2P شما" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

عزیز ${name}

شما سفارش خود را به عنوان پرداخت شده علامت‌گذاری کرده‌اید. در انتظار فروشنده برای بررسی، تأیید و آزادسازی وجوه

از اینکه از پلتفرم ما استفاده می‌کنید سپاسگزاریم.

درخواست از: ${ip}

با احترام
تیم ${api_name}

", + "title": "فعالیت شناسایی شده در سفارش P2P شما" + }, + "P2P_ORDER_EXPIRED": { + "html": "

عزیز ${name}

سفارش شما به دلیل عدم فعالیت در زمان داده شده منقضی شده است.

از اینکه از پلتفرم ما استفاده می‌کنید سپاسگزاریم.

درخواست از: ${ip}

با احترام
تیم ${api_name}

", + "title": "فعالیت شناسایی شده در سفارش P2P شما" + }, + "P2P_ORDER_CLOSED": { + "html": "

عزیز ${name}

سفارش شما توسط تیم پشتیبانی ما پس از رسیدن به نتیجه در درخواست تجدیدنظر بسته شده است

از اینکه از پلتفرم ما استفاده می‌کنید متشکریم.

با احترام
تیم ${api_name}

", + "title": "فعالیت در سفارش P2P شما شناسایی شد" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

عزیز ${name}

شما سفارش خود را لغو کرده‌اید. معامله بسته شده است.

از اینکه از پلتفرم ما استفاده می‌کنید سپاسگزاریم.

درخواست از: ${ip}

با احترام
تیم ${api_name}

", + "title": "فعالیت شناسایی شده در سفارش P2P شما" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

عزیز ${name}

شما به سفارش خود اعتراض کرده‌اید. لطفاً با پشتیبانی تماس بگیرید تا مشکل حل شود.

درخواست از: ${ip}

با احترام
تیم ${api_name}

", + "title": "فعالیت شناسایی شده در سفارش P2P شما" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

عزیز ${name}

شما معامله را تأیید کرده و وجوه را آزاد کرده‌اید.

از اینکه از پلتفرم ما استفاده می‌کنید سپاسگزاریم.

درخواست از: ${ip}

با احترام
تیم ${api_name}

", + "title": "فعالیت شناسایی شده در سفارش P2P شما" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

عزیز ${name}

شما سفارش خود را لغو کرده‌اید. معامله بسته شده است.

از اینکه از پلتفرم ما استفاده می‌کنید سپاسگزاریم.

درخواست از: ${ip}

با احترام
تیم ${api_name}

", + "title": "فعالیت شناسایی شده در سفارش P2P شما" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

عزیز ${name}

شما به سفارش خود اعتراض کرده‌اید. لطفاً با پشتیبانی تماس بگیرید تا مشکل حل شود.

از اینکه از پلتفرم ما استفاده می‌کنید سپاسگزاریم.

درخواست از: ${ip}

با احترام
تیم ${api_name}

", + "title": "فعالیت شناسایی شده در سفارش P2P شما" } } diff --git a/server/mail/strings/fr.json b/server/mail/strings/fr.json index 6c63b3050e..688c11bb63 100644 --- a/server/mail/strings/fr.json +++ b/server/mail/strings/fr.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

Cher/Chère ${name}

Nous avons enregistré que votre authentification à deux facteurs (2FA) a été activée sur votre compte.

Heure: ${time}
Pays: ${country}
Adresse IP: ${ip}

Cordialement
Équipe ${api_name}

", "title": "OTP Activé" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

Cher ${name}

Nous avons détecté une activité sur votre transaction P2P, ce qui peut indiquer qu'un client demande à effectuer une transaction avec vous. Pour vérifier le statut de votre transaction, veuillez cliquer sur le lien ci-dessous:

Merci d'utiliser notre plateforme.

Requête initiée depuis: ${ip}

Cordialement
L'équipe ${api_name}

", + "title": "Mise à jour sur votre transaction P2P" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

Cher ${name}

Vous avez marqué votre commande comme payée. En attente de la vérification, confirmation et libération des fonds par le vendeur

Merci d'utiliser notre plateforme.

Requête initiée depuis: ${ip}

Cordialement
L'équipe ${api_name}

", + "title": "Activité détectée sur votre commande P2P" + }, + "P2P_ORDER_EXPIRED": { + "html": "

Cher ${name}

Votre commande a expiré en raison d'une inactivité dans le délai imparti.

Merci d'utiliser notre plateforme.

Requête initiée depuis: ${ip}

Cordialement
L'équipe ${api_name}

", + "title": "Activité détectée sur votre commande P2P" + }, + "P2P_ORDER_CLOSED": { + "html": "

Cher ${name}

Votre commande a été clôturée par notre équipe de support après avoir rendu un verdict sur la demande d'appel

Merci d'utiliser notre plateforme.

Cordialement
L'équipe ${api_name}

", + "title": "Activité détectée sur votre commande P2P" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

Cher ${name}

Vous avez annulé votre commande. La transaction est fermée.

Merci d'utiliser notre plateforme.

Requête initiée depuis: ${ip}

Cordialement
L'équipe ${api_name}

", + "title": "Activité détectée sur votre commande P2P" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

Cher ${name}

Vous avez fait appel de votre commande. Veuillez contacter le support pour résoudre le problème.

Requête initiée depuis: ${ip}

Cordialement
L'équipe ${api_name}

", + "title": "Activité détectée sur votre commande P2P" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

Cher ${name}

Vous avez confirmé la transaction et libéré les fonds.

Merci d'utiliser notre plateforme.

Requête initiée depuis: ${ip}

Cordialement
L'équipe ${api_name}

", + "title": "Activité détectée sur votre commande P2P" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

Cher ${name}

Vous avez annulé votre commande. La transaction est fermée.

Merci d'utiliser notre plateforme.

Requête initiée depuis: ${ip}

Cordialement
L'équipe ${api_name}

", + "title": "Activité détectée sur votre commande P2P" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

Cher ${name}

Vous avez fait appel de votre commande. Veuillez contacter le support pour résoudre le problème.

Merci d'utiliser notre plateforme.

Requête initiée depuis: ${ip}

Cordialement
L'équipe ${api_name}

", + "title": "Activité détectée sur votre commande P2P" } } } \ No newline at end of file diff --git a/server/mail/strings/id.json b/server/mail/strings/id.json index ea66450d89..9b6e4a52e7 100644 --- a/server/mail/strings/id.json +++ b/server/mail/strings/id.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

प्रिय ${name}

हमने देखा है कि आपके खाते पर दो-कारक प्रमाणीकरण (2FA) सक्रिय हो गया है

समय: ${time}
देश: ${country}
IP पता: ${ip}

शुभकामनाएं
${api_name} टीम

", "title": "2FA सक्रिय किया गया" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

Dear ${name}

Kami telah mendeteksi aktivitas pada transaksi P2P Anda, yang mungkin menunjukkan adanya permintaan dari pelanggan untuk bertransaksi dengan Anda. Untuk memeriksa status transaksi Anda, silakan klik tautan di bawah ini:

Terima kasih telah menggunakan platform kami.

Permintaan diajukan dari: ${ip}

Salam Hormat
tim ${api_name}

", + "title": "Pembaruan tentang Transaksi P2P Anda" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

Dear ${name}

Anda telah menandai pesanan Anda sebagai telah dibayar. Menunggu penjual untuk memeriksa, mengkonfirmasi, dan melepaskan dana

Terima kasih telah menggunakan platform kami.

Permintaan diajukan dari: ${ip}

Salam Hormat
tim ${api_name}

", + "title": "Aktivitas Terdeteksi pada Pesanan P2P Anda" + }, + "P2P_ORDER_EXPIRED": { + "html": "

Dear ${name}

Pesanan Anda telah kedaluwarsa karena tidak ada aktivitas dalam waktu yang diberikan.

Terima kasih telah menggunakan platform kami.

Permintaan diajukan dari: ${ip}

Salam Hormat
tim ${api_name}

", + "title": "Aktivitas Terdeteksi pada Pesanan P2P Anda" + }, + "P2P_ORDER_CLOSED": { + "html": "

Dear ${name}

Pesanan Anda telah ditutup oleh tim dukungan kami setelah mencapai keputusan atas permintaan banding

Terima kasih telah menggunakan platform kami.

Hormat kami
tim ${api_name}

", + "title": "Aktivitas terdeteksi pada Pesanan P2P Anda" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

Dear ${name}

Anda telah membatalkan pesanan Anda. Transaksi ditutup.

Terima kasih telah menggunakan platform kami.

Permintaan diajukan dari: ${ip}

Salam Hormat
tim ${api_name}

", + "title": "Aktivitas Terdeteksi pada Pesanan P2P Anda" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

Dear ${name}

Anda telah mengajukan banding untuk pesanan Anda. Silakan hubungi dukungan untuk menyelesaikan masalah ini.

Permintaan diajukan dari: ${ip}

Salam Hormat
tim ${api_name}

", + "title": "Aktivitas Terdeteksi pada Pesanan P2P Anda" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

Dear ${name}

Anda telah mengkonfirmasi transaksi dan melepaskan dana.

Terima kasih telah menggunakan platform kami.

Permintaan diajukan dari: ${ip}

Salam Hormat
tim ${api_name}

", + "title": "Aktivitas Terdeteksi pada Pesanan P2P Anda" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

Dear ${name}

Anda telah membatalkan pesanan Anda. Transaksi ditutup.

Terima kasih telah menggunakan platform kami.

Permintaan diajukan dari: ${ip}

Salam Hormat
tim ${api_name}

", + "title": "Aktivitas Terdeteksi pada Pesanan P2P Anda" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

Dear ${name}

Anda telah mengajukan banding untuk pesanan Anda. Silakan hubungi dukungan untuk menyelesaikan masalah ini.

Terima kasih telah menggunakan platform kami.

Permintaan diajukan dari: ${ip}

Salam Hormat
tim ${api_name}

", + "title": "Aktivitas Terdeteksi pada Pesanan P2P Anda" } } } \ No newline at end of file diff --git a/server/mail/strings/index.js b/server/mail/strings/index.js index 684a4456e7..cac55a91a3 100644 --- a/server/mail/strings/index.js +++ b/server/mail/strings/index.js @@ -54,6 +54,17 @@ const MAILTYPE = { // OTP OTP_DISABLED: 'otp_disabled', OTP_ENABLED: 'otp_enabled', + + //P2P + P2P_MERCHANT_IN_PROGRESS: 'p2p_merchant_in_progress', + P2P_BUYER_PAID_ORDER: 'p2p_buyer_paid_order', + P2P_ORDER_EXPIRED: 'p2p_order_expired', + P2P_ORDER_CLOSED: 'p2p_order_closed', + P2P_BUYER_CANCELLED_ORDER: 'p2p_buyer_cancelled_order', + P2P_BUYER_APPEALED_ORDER: 'p2p_buyer_appealed_order', + P2P_VENDOR_CONFIRMED_ORDER: 'p2p_vendor_confirmed_order', + P2P_VENDOR_CANCELLED_ORDER: 'p2p_vendor_cancelled_order', + P2P_VENDOR_APPEALED_ORDER: 'p2p_vendor_appealed_order', }; const languageFile = (lang) => { diff --git a/server/mail/strings/ja.json b/server/mail/strings/ja.json index 806a75f5fb..143fa7442a 100644 --- a/server/mail/strings/ja.json +++ b/server/mail/strings/ja.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

${name} 様

お客様のアカウントで二段階認証(2FA)が有効になったことを記録しました

時間: ${time}
国: ${country}
IPアドレス: ${ip}

敬具
${api_name} チーム

", "title": "OTP 有効化" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

親愛なる${name}様

お客様のP2P取引にアクティビティが検出されました。これは、顧客が取引を要求している可能性があります。取引のステータスを確認するには、以下のリンクをクリックしてください:

私たちのプラットフォームをご利用いただきありがとうございます。

リクエスト発行元: ${ip}

敬具
${api_name}チーム

", + "title": "P2P取引の更新情報" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

親愛なる${name}様

お客様は注文を支払い済みとマークしました。ベンダーが確認し、資金を解放するのを待っています

私たちのプラットフォームをご利用いただきありがとうございます。

リクエスト発行元: ${ip}

敬具
${api_name}チーム

", + "title": "P2P注文にアクティビティが検出されました" + }, + "P2P_ORDER_EXPIRED": { + "html": "

親愛なる${name}様

お客様の注文は、指定された時間内にアクティビティがなかったために期限切れとなりました。

私たちのプラットフォームをご利用いただきありがとうございます。

リクエスト発行元: ${ip}

敬具
${api_name}チーム

", + "title": "P2P注文にアクティビティが検出されました" + }, + "P2P_ORDER_CLOSED": { + "html": "

親愛なる${name}様

サポートチームが異議申し立てのリクエストについて判決を出した後、あなたの注文は閉じられました

私たちのプラットフォームをご利用いただきありがとうございます。

敬具
${api_name}チーム

", + "title": "P2P注文に活動が検出されました" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

親愛なる${name}様

お客様は注文をキャンセルしました。取引は終了しました。

私たちのプラットフォームをご利用いただきありがとうございます。

リクエスト発行元: ${ip}

敬具
${api_name}チーム

", + "title": "P2P注文にアクティビティが検出されました" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

親愛なる${name}様

お客様は注文に対して異議を申し立てました。問題を解決するためにサポートに連絡してください。

リクエスト発行元: ${ip}

敬具
${api_name}チーム

", + "title": "P2P注文にアクティビティが検出されました" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

親愛なる${name}様

お客様は取引を確認し、資金を解放しました。

私たちのプラットフォームをご利用いただきありがとうございます。

リクエスト発行元: ${ip}

敬具
${api_name}チーム

", + "title": "P2P注文にアクティビティが検出されました" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

親愛なる${name}様

お客様は注文をキャンセルしました。取引は終了しました。

私たちのプラットフォームをご利用いただきありがとうございます。

リクエスト発行元: ${ip}

敬具
${api_name}チーム

", + "title": "P2P注文にアクティビティが検出されました" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

親愛なる${name}様

お客様は注文に対して異議を申し立てました。問題を解決するためにサポートに連絡してください。

私たちのプラットフォームをご利用いただきありがとうございます。

リクエスト発行元: ${ip}

敬具
${api_name}チーム

", + "title": "P2P注文にアクティビティが検出されました" } } } \ No newline at end of file diff --git a/server/mail/strings/ko.json b/server/mail/strings/ko.json index 757c7ab490..b63a8c31fe 100644 --- a/server/mail/strings/ko.json +++ b/server/mail/strings/ko.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

${name} 님

귀하의 계정에서 이중 인증 (2FA)이 활성화되었다는 것을 기록했습니다.

시간: ${time}
국가: ${country}
IP 주소: ${ip}

성의를 다하여
${api_name} 팀

", "title": "OTP 활성화됨" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

친애하는 ${name} 님

귀하의 P2P 거래에 활동이 감지되었습니다. 이는 고객이 귀하와 거래를 요청하고 있음을 나타낼 수 있습니다. 거래 상태를 확인하려면 아래 링크를 클릭하십시오:

저희 플랫폼을 이용해 주셔서 감사합니다.

요청이 발생한 위치: ${ip}

감사합니다
${api_name} 팀

", + "title": "귀하의 P2P 거래 업데이트" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

친애하는 ${name} 님

귀하는 주문을 결제 완료로 표시했습니다. 판매자가 확인하고 자금을 해제하기를 기다리고 있습니다

저희 플랫폼을 이용해 주셔서 감사합니다.

요청이 발생한 위치: ${ip}

감사합니다
${api_name} 팀

", + "title": "귀하의 P2P 주문에서 감지된 활동" + }, + "P2P_ORDER_EXPIRED": { + "html": "

친애하는 ${name} 님

지정된 시간 내에 활동이 없어서 귀하의 주문이 만료되었습니다.

저희 플랫폼을 이용해 주셔서 감사합니다.

요청이 발생한 위치: ${ip}

감사합니다
${api_name} 팀

", + "title": "귀하의 P2P 주문에서 감지된 활동" + }, + "P2P_ORDER_CLOSED": { + "html": "

친애하는 ${name} 님

지원 팀이 항소 요청에 대한 판결을 내린 후 주문이 종료되었습니다

저희 플랫폼을 이용해 주셔서 감사합니다.

감사합니다
${api_name} 팀

", + "title": "P2P 주문에 활동이 감지되었습니다" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

친애하는 ${name} 님

귀하는 주문을 취소했습니다. 거래가 종료되었습니다.

저희 플랫폼을 이용해 주셔서 감사합니다.

요청이 발생한 위치: ${ip}

감사합니다
${api_name} 팀

", + "title": "귀하의 P2P 주문에서 감지된 활동" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

친애하는 ${name} 님

귀하는 주문에 대해 이의를 제기했습니다. 문제를 해결하려면 지원팀에 문의하십시오.

요청이 발생한 위치: ${ip}

감사합니다
${api_name} 팀

", + "title": "귀하의 P2P 주문에서 감지된 활동" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

친애하는 ${name} 님

귀하는 거래를 확인하고 자금을 해제했습니다.

저희 플랫폼을 이용해 주셔서 감사합니다.

요청이 발생한 위치: ${ip}

감사합니다
${api_name} 팀

", + "title": "귀하의 P2P 주문에서 감지된 활동" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

친애하는 ${name} 님

귀하는 주문을 취소했습니다. 거래가 종료되었습니다.

저희 플랫폼을 이용해 주셔서 감사합니다.

요청이 발생한 위치: ${ip}

감사합니다
${api_name} 팀

", + "title": "귀하의 P2P 주문에서 감지된 활동" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

친애하는 ${name} 님

귀하는 주문에 대해 이의를 제기했습니다. 문제를 해결하려면 지원팀에 문의하십시오.

저희 플랫폼을 이용해 주셔서 감사합니다.

요청이 발생한 위치: ${ip}

감사합니다
${api_name} 팀

", + "title": "귀하의 P2P 주문에서 감지된 활동" } } } \ No newline at end of file diff --git a/server/mail/strings/mn.json b/server/mail/strings/mn.json index 6d932f1270..163402204b 100644 --- a/server/mail/strings/mn.json +++ b/server/mail/strings/mn.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

${name}

Таны дансны хоёр чухал хамгаалалт (2FA) идэвхлэгдсэн байна гэж бичлэгт орууллаа.

Цаг: ${time}
Улс: ${country}
IP Хаяг: ${ip}

Таны нэртэй
${api_name} баг

", "title": "OTP Идэвхлэгдсэн" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

Эрхэм ${name}

Таны P2P гүйлгээнд үйл ажиллагаа илэрсэн байна. Энэ нь хэрэглэгч таныг гүйлгээ хийхийг хүсч байгааг илэрхийлж болно. Гүйлгээний байдлыг шалгахын тулд доорх холбоос дээр дарна уу:

Манай платформыг ашигласанд баярлалаа.

Хүсэлт ирсэн IP: ${ip}

Хүндэтгэсэн
${api_name} баг

", + "title": "Таны P2P гүйлгээний шинэчлэлт" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

Эрхэм ${name}

Та захиалгаа төлөгдсөн гэж тэмдэглэсэн байна. Худалдагч баталгаажуулж, мөнгийг чөлөөлөхийг хүлээж байна

Манай платформыг ашигласанд баярлалаа.

Хүсэлт ирсэн IP: ${ip}

Хүндэтгэсэн
${api_name} баг

", + "title": "Таны P2P захиалгад үйл ажиллагаа илэрсэн байна" + }, + "P2P_ORDER_EXPIRED": { + "html": "

Эрхэм ${name}

Таны захиалга заасан хугацаанд үйл ажиллагаа байхгүй учир дууссан байна.

Манай платформыг ашигласанд баярлалаа.

Хүсэлт ирсэн IP: ${ip}

Хүндэтгэсэн
${api_name} баг

", + "title": "Таны P2P захиалгад үйл ажиллагаа илэрсэн байна" + }, + "P2P_ORDER_CLOSED": { + "html": "

Эрхэм ${name}

Таны захиалгыг манай дэмжлэгийн баг таны давж заалдах хүсэлтийн талаар шийдвэр гаргасны дараа хаасан байна

Манай платформыг ашигласанд баярлалаа.

Хүндэтгэсэн
${api_name} баг

", + "title": "Таны P2P захиалгад үйл ажиллагаа илэрсэн байна" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

Эрхэм ${name}

Та захиалгаа цуцалсан байна. Гүйлгээ хаагдсан байна.

Манай платформыг ашигласанд баярлалаа.

Хүсэлт ирсэн IP: ${ip}

Хүндэтгэсэн
${api_name} баг

", + "title": "Таны P2P захиалгад үйл ажиллагаа илэрсэн байна" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

Эрхэм ${name}

Та захиалгандаа гомдол гаргасан байна. Асуудлыг шийдвэрлэхийн тулд дэмжлэгтэй холбогдоно уу.

Хүсэлт ирсэн IP: ${ip}

Хүндэтгэсэн
${api_name} баг

", + "title": "Таны P2P захиалгад үйл ажиллагаа илэрсэн байна" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

Эрхэм ${name}

Та гүйлгээг баталгаажуулж, мөнгийг чөлөөлсөн байна.

Манай платформыг ашигласанд баярлалаа.

Хүсэлт ирсэн IP: ${ip}

Хүндэтгэсэн
${api_name} баг

", + "title": "Таны P2P захиалгад үйл ажиллагаа илэрсэн байна" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

Эрхэм ${name}

Та захиалгаа цуцалсан байна. Гүйлгээ хаагдсан байна.

Манай платформыг ашигласанд баярлалаа.

Хүсэлт ирсэн IP: ${ip}

Хүндэтгэсэн
${api_name} баг

", + "title": "Таны P2P захиалгад үйл ажиллагаа илэрсэн байна" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

Эрхэм ${name}

Та захиалгандаа гомдол гаргасан байна. Асуудлыг шийдвэрлэхийн тулд дэмжлэгтэй холбогдоно уу.

Манай платформыг ашигласанд баярлалаа.

Хүсэлт ирсэн IP: ${ip}

Хүндэтгэсэн
${api_name} баг

", + "title": "Таны P2P захиалгад үйл ажиллагаа илэрсэн байна" } } } \ No newline at end of file diff --git a/server/mail/strings/pt.json b/server/mail/strings/pt.json index ff336d8d26..9288f8936a 100644 --- a/server/mail/strings/pt.json +++ b/server/mail/strings/pt.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

Prezado/a ${name}

Registramos que a sua autenticação de dois fatores (2FA) foi ativada na sua conta.

Horário: ${time}
País: ${country}
Endereço IP: ${ip}

Atenciosamente
Equipe ${api_name}

", "title": "OTP Ativado" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

Caro(a) ${name}

Detectamos atividade em sua transação P2P, o que pode indicar que um cliente está solicitando uma transação com você. Para verificar o status da sua transação, clique no link abaixo:

Obrigado por usar nossa plataforma.

Solicitação iniciada de: ${ip}

Atenciosamente
equipe ${api_name}

", + "title": "Atualização sobre sua Transação P2P" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

Caro(a) ${name}

Você marcou seu pedido como pago. Aguardando o vendedor verificar, confirmar e liberar os fundos

Obrigado por usar nossa plataforma.

Solicitação iniciada de: ${ip}

Atenciosamente
equipe ${api_name}

", + "title": "Atividade Detectada em seu Pedido P2P" + }, + "P2P_ORDER_EXPIRED": { + "html": "

Caro(a) ${name}

Seu pedido expirou devido à inatividade no tempo determinado.

Obrigado por usar nossa plataforma.

Solicitação iniciada de: ${ip}

Atenciosamente
equipe ${api_name}

", + "title": "Atividade Detectada em seu Pedido P2P" + }, + "P2P_ORDER_CLOSED": { + "html": "

Caro(a) ${name}

Seu pedido foi encerrado pela nossa equipe de suporte após chegar a um veredicto sobre a solicitação de apelação

Obrigado por usar nossa plataforma.

Atenciosamente
equipe ${api_name}

", + "title": "Atividade Detectada em seu Pedido P2P" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

Caro(a) ${name}

Você cancelou seu pedido. A transação foi encerrada.

Obrigado por usar nossa plataforma.

Solicitação iniciada de: ${ip}

Atenciosamente
equipe ${api_name}

", + "title": "Atividade Detectada em seu Pedido P2P" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

Caro(a) ${name}

Você recorreu do seu pedido. Por favor, entre em contato com o suporte para resolver o problema.

Solicitação iniciada de: ${ip}

Atenciosamente
equipe ${api_name}

", + "title": "Atividade Detectada em seu Pedido P2P" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

Caro(a) ${name}

Você confirmou a transação e liberou os fundos.

Obrigado por usar nossa plataforma.

Solicitação iniciada de: ${ip}

Atenciosamente
equipe ${api_name}

", + "title": "Atividade Detectada em seu Pedido P2P" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

Caro(a) ${name}

Você cancelou seu pedido. A transação foi encerrada.

Obrigado por usar nossa plataforma.

Solicitação iniciada de: ${ip}

Atenciosamente
equipe ${api_name}

", + "title": "Atividade Detectada em seu Pedido P2P" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

Caro(a) ${name}

Você recorreu do seu pedido. Por favor, entre em contato com o suporte para resolver o problema.

Obrigado por usar nossa plataforma.

Solicitação iniciada de: ${ip}

Atenciosamente
equipe ${api_name}

", + "title": "Atividade Detectada em seu Pedido P2P" } } } \ No newline at end of file diff --git a/server/mail/strings/tr.json b/server/mail/strings/tr.json index ed86a923ed..b34439845c 100644 --- a/server/mail/strings/tr.json +++ b/server/mail/strings/tr.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

Sayın ${name}

Hesabınızda iki faktörlü kimlik doğrulama (2FA) etkinleştirildi olarak kaydedilmiştir.

Zaman: ${time}
Ülke: ${country}
IP Adresi: ${ip}

Saygılarımla
${api_name} Ekibi

", "title": "OTP Etkin" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

Sevgili ${name}

P2P işleminizde bir aktivite tespit ettik, bu da bir müşterinin sizinle işlem yapmak istediğini gösterebilir. İşleminizin durumunu kontrol etmek için lütfen aşağıdaki bağlantıya tıklayın:

Platformumuzu kullandığınız için teşekkür ederiz.

İstek şu IP'den yapıldı: ${ip}

Saygılarımızla
${api_name} ekibi

", + "title": "P2P İşleminizle İlgili Güncelleme" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

Sevgili ${name}

Siparişinizi ödenmiş olarak işaretlediniz. Satıcının kontrol etmesini, onaylamasını ve fonları serbest bırakmasını bekliyoruz

Platformumuzu kullandığınız için teşekkür ederiz.

İstek şu IP'den yapıldı: ${ip}

Saygılarımızla
${api_name} ekibi

", + "title": "P2P Siparişinizde Aktivite Tespit Edildi" + }, + "P2P_ORDER_EXPIRED": { + "html": "

Sevgili ${name}

Belirtilen süre içinde aktivite olmadığı için siparişinizin süresi doldu.

Platformumuzu kullandığınız için teşekkür ederiz.

İstek şu IP'den yapıldı: ${ip}

Saygılarımızla
${api_name} ekibi

", + "title": "P2P Siparişinizde Aktivite Tespit Edildi" + }, + "P2P_ORDER_CLOSED": { + "html": "

Sevgili ${name}

Destek ekibimiz, itiraz talebi hakkında bir karara vardıktan sonra siparişinizi kapattı

Platformumuzu kullandığınız için teşekkür ederiz.

Saygılarımızla
${api_name} ekibi

", + "title": "P2P Siparişinizde Aktivite Tespit Edildi" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

Sevgili ${name}

Siparişinizi iptal ettiniz. İşlem kapatıldı.

Platformumuzu kullandığınız için teşekkür ederiz.

İstek şu IP'den yapıldı: ${ip}

Saygılarımızla
${api_name} ekibi

", + "title": "P2P Siparişinizde Aktivite Tespit Edildi" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

Sevgili ${name}

Siparişinize itiraz ettiniz. Sorunu çözmek için lütfen destek ile iletişime geçin.

İstek şu IP'den yapıldı: ${ip}

Saygılarımızla
${api_name} ekibi

", + "title": "P2P Siparişinizde Aktivite Tespit Edildi" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

Sevgili ${name}

İşlemi onayladınız ve fonları serbest bıraktınız.

Platformumuzu kullandığınız için teşekkür ederiz.

İstek şu IP'den yapıldı: ${ip}

Saygılarımızla
${api_name} ekibi

", + "title": "P2P Siparişinizde Aktivite Tespit Edildi" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

Sevgili ${name}

Siparişinizi iptal ettiniz. İşlem kapatıldı.

Platformumuzu kullandığınız için teşekkür ederiz.

İstek şu IP'den yapıldı: ${ip}

Saygılarımızla
${api_name} ekibi

", + "title": "P2P Siparişinizde Aktivite Tespit Edildi" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

Sevgili ${name}

Siparişinize itiraz ettiniz. Sorunu çözmek için lütfen destek ile iletişime geçin.

Platformumuzu kullandığınız için teşekkür ederiz.

İstek şu IP'den yapıldı: ${ip}

Saygılarımızla
${api_name} ekibi

", + "title": "P2P Siparişinizde Aktivite Tespit Edildi" } } } diff --git a/server/mail/strings/ur.json b/server/mail/strings/ur.json index 2320d3db74..2d4b0975a1 100644 --- a/server/mail/strings/ur.json +++ b/server/mail/strings/ur.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

عزیز ${name}

ہم نے ریکارڈ کیا ہے کہ آپ کی دو مرحلی تصدیق (2FA) فعال کردی گئی ہے آپ کے اکاؤنٹ پر۔

وقت: ${time}
ملک: ${country}
IP پتہ: ${ip}

خیریت واحترام
${api_name} ٹیم

", "title": "OTP فعال" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

محترم ${name}

ہم نے آپ کی P2P ڈیل پر سرگرمی کا پتہ لگایا ہے، جس سے ظاہر ہوتا ہے کہ ایک کسٹمر آپ کے ساتھ ٹرانزیکٹ کرنا چاہتا ہے۔ اپنی ڈیل کی حالت چیک کرنے کے لئے نیچے دیئے گئے لنک پر کلک کریں:

ہمارا پلیٹ فارم استعمال کرنے کا شکریہ۔

درخواست شروع کی گئی IP: ${ip}

نیک تمنائیں
${api_name} ٹیم

", + "title": "آپ کی P2P ڈیل کی تازہ کاری" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

محترم ${name}

آپ نے اپنا آرڈر ادا کیا ہوا نشان زد کیا ہے۔ فروخت کنندہ کے چیک، تصدیق اور فنڈز جاری کرنے کا انتظار

ہمارا پلیٹ فارم استعمال کرنے کا شکریہ۔

درخواست شروع کی گئی IP: ${ip}

نیک تمنائیں
${api_name} ٹیم

", + "title": "آپ کے P2P آرڈر میں سرگرمی کا پتہ چلا ہے" + }, + "P2P_ORDER_EXPIRED": { + "html": "

محترم ${name}

آپ کے آرڈر کا وقت مکمل ہو گیا ہے کیونکہ دی گئی مدت میں کوئی سرگرمی نہیں ہوئی۔

ہمارا پلیٹ فارم استعمال کرنے کا شکریہ۔

درخواست شروع کی گئی IP: ${ip}

نیک تمنائیں
${api_name} ٹیم

", + "title": "آپ کے P2P آرڈر میں سرگرمی کا پتہ چلا ہے" + }, + "P2P_ORDER_CLOSED": { + "html": "

محترم ${name}

آپ کے اپیل کی درخواست پر فیصلہ آنے کے بعد آپ کا آرڈر ہماری سپورٹ ٹیم نے بند کر دیا ہے

ہمارا پلیٹ فارم استعمال کرنے کا شکریہ۔

نیک تمنائیں
${api_name} ٹیم

", + "title": "آپ کے P2P آرڈر میں سرگرمی کا پتہ چلا ہے" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

محترم ${name}

آپ نے اپنا آرڈر منسوخ کر دیا ہے۔ ٹرانزیکشن بند ہو گئی ہے۔

ہمارا پلیٹ فارم استعمال کرنے کا شکریہ۔

درخواست شروع کی گئی IP: ${ip}

نیک تمنائیں
${api_name} ٹیم

", + "title": "آپ کے P2P آرڈر میں سرگرمی کا پتہ چلا ہے" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

محترم ${name}

آپ نے اپنے آرڈر پر اپیل کی ہے۔ مسئلہ حل کرنے کے لئے برائے مہربانی سپورٹ سے رابطہ کریں۔

درخواست شروع کی گئی IP: ${ip}

نیک تمنائیں
${api_name} ٹیم

", + "title": "آپ کے P2P آرڈر میں سرگرمی کا پتہ چلا ہے" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

محترم ${name}

آپ نے ٹرانزیکشن کی تصدیق کی ہے اور فنڈز جاری کیے ہیں۔

ہمارا پلیٹ فارم استعمال کرنے کا شکریہ۔

درخواست شروع کی گئی IP: ${ip}

نیک تمنائیں
${api_name} ٹیم

", + "title": "آپ کے P2P آرڈر میں سرگرمی کا پتہ چلا ہے" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

محترم ${name}

آپ نے اپنا آرڈر منسوخ کر دیا ہے۔ ٹرانزیکشن بند ہو گئی ہے۔

ہمارا پلیٹ فارم استعمال کرنے کا شکریہ۔

درخواست شروع کی گئی IP: ${ip}

نیک تمنائیں
${api_name} ٹیم

", + "title": "آپ کے P2P آرڈر میں سرگرمی کا پتہ چلا ہے" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

محترم ${name}

آپ نے اپنے آرڈر پر اپیل کی ہے۔ مسئلہ حل کرنے کے لئے برائے مہربانی سپورٹ سے رابطہ کریں۔

ہمارا پلیٹ فارم استعمال کرنے کا شکریہ۔

درخواست شروع کی گئی IP: ${ip}

نیک تمنائیں
${api_name} ٹیم

", + "title": "آپ کے P2P آرڈر میں سرگرمی کا پتہ چلا ہے" } } } diff --git a/server/mail/strings/vi.json b/server/mail/strings/vi.json index 4ea2b84ef2..7e01ce8258 100644 --- a/server/mail/strings/vi.json +++ b/server/mail/strings/vi.json @@ -123,6 +123,42 @@ "OTP_ENABLED": { "html": "

Kính gửi ${name}

Chúng tôi đã ghi nhận rằng xác thực hai yếu tố (2FA) đã được kích hoạt trên tài khoản của bạn.

Thời gian: ${time}
Quốc gia: ${country}
Địa chỉ IP: ${ip}

Trân trọng
Đội ${api_name}

", "title": "OTP Đã Kích Hoạt" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

Kính gửi ${name}

Chúng tôi đã phát hiện hoạt động trên giao dịch P2P của bạn, điều này có thể cho thấy rằng một khách hàng đang yêu cầu giao dịch với bạn. Để kiểm tra trạng thái giao dịch của bạn, vui lòng nhấp vào liên kết dưới đây:

Cảm ơn bạn đã sử dụng nền tảng của chúng tôi.

Yêu cầu được thực hiện từ: ${ip}

Trân trọng
đội ngũ ${api_name}

", + "title": "Cập nhật về Giao dịch P2P của bạn" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

Kính gửi ${name}

Bạn đã đánh dấu đơn hàng của mình là đã thanh toán. Đang chờ người bán kiểm tra, xác nhận và giải phóng quỹ

Cảm ơn bạn đã sử dụng nền tảng của chúng tôi.

Yêu cầu được thực hiện từ: ${ip}

Trân trọng
đội ngũ ${api_name}

", + "title": "Hoạt động được phát hiện trên Đơn hàng P2P của bạn" + }, + "P2P_ORDER_EXPIRED": { + "html": "

Kính gửi ${name}

Đơn hàng của bạn đã hết hạn do không có hoạt động trong thời gian quy định.

Cảm ơn bạn đã sử dụng nền tảng của chúng tôi.

Yêu cầu được thực hiện từ: ${ip}

Trân trọng
đội ngũ ${api_name}

", + "title": "Hoạt động được phát hiện trên Đơn hàng P2P của bạn" + }, + "P2P_ORDER_CLOSED": { + "html": "

Kính gửi ${name}

Đơn hàng của bạn đã được đội ngũ hỗ trợ của chúng tôi đóng lại sau khi đạt được quyết định về yêu cầu kháng cáo

Cảm ơn bạn đã sử dụng nền tảng của chúng tôi.

Trân trọng
đội ngũ ${api_name}

", + "title": "Hoạt động được phát hiện trên Đơn hàng P2P của bạn" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

Kính gửi ${name}

Bạn đã hủy đơn hàng của mình. Giao dịch đã đóng.

Cảm ơn bạn đã sử dụng nền tảng của chúng tôi.

Yêu cầu được thực hiện từ: ${ip}

Trân trọng
đội ngũ ${api_name}

", + "title": "Hoạt động được phát hiện trên Đơn hàng P2P của bạn" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

Kính gửi ${name}

Bạn đã khiếu nại đơn hàng của mình. Vui lòng liên hệ với bộ phận hỗ trợ để giải quyết vấn đề.

Yêu cầu được thực hiện từ: ${ip}

Trân trọng
đội ngũ ${api_name}

", + "title": "Hoạt động được phát hiện trên Đơn hàng P2P của bạn" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

Kính gửi ${name}

Bạn đã xác nhận giao dịch và giải phóng quỹ.

Cảm ơn bạn đã sử dụng nền tảng của chúng tôi.

Yêu cầu được thực hiện từ: ${ip}

Trân trọng
đội ngũ ${api_name}

", + "title": "Hoạt động được phát hiện trên Đơn hàng P2P của bạn" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

Kính gửi ${name}

Bạn đã hủy đơn hàng của mình. Giao dịch đã đóng.

Cảm ơn bạn đã sử dụng nền tảng của chúng tôi.

Yêu cầu được thực hiện từ: ${ip}

Trân trọng
đội ngũ ${api_name}

", + "title": "Hoạt động được phát hiện trên Đơn hàng P2P của bạn" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

Kính gửi ${name}

Bạn đã khiếu nại đơn hàng của mình. Vui lòng liên hệ với bộ phận hỗ trợ để giải quyết vấn đề.

Cảm ơn bạn đã sử dụng nền tảng của chúng tôi.

Yêu cầu được thực hiện từ: ${ip}

Trân trọng
đội ngũ ${api_name}

", + "title": "Hoạt động được phát hiện trên Đơn hàng P2P của bạn" } } } \ No newline at end of file diff --git a/server/mail/strings/zh.json b/server/mail/strings/zh.json index e541d3f38c..086872cbc8 100644 --- a/server/mail/strings/zh.json +++ b/server/mail/strings/zh.json @@ -119,6 +119,42 @@ "OTP_ENABLED": { "html": "

尊敬的 ${name} 先生/女士

我们记录到您的账户上的 双因素认证 (2FA) 已经被启用

时间:${time}
国家:${country}
IP 地址:${ip}

敬启
${api_name} 团队

", "title": "OTP 已启用" + }, + "P2P_MERCHANT_IN_PROGRESS": { + "html": "

亲爱的 ${name}

我们检测到您的P2P交易中有活动,这可能表明有客户请求与您进行交易。要检查您的交易状态,请点击以下链接:

感谢您使用我们的平台。

请求发起自:${ip}

此致
${api_name}团队

", + "title": "您的P2P交易更新" + }, + "P2P_BUYER_PAID_ORDER": { + "html": "

亲爱的 ${name}

您已将订单标记为已付款。等待卖家检查、确认并释放资金

感谢您使用我们的平台。

请求发起自:${ip}

此致
${api_name}团队

", + "title": "您的P2P订单中检测到活动" + }, + "P2P_ORDER_EXPIRED": { + "html": "

亲爱的 ${name}

由于在规定时间内没有活动,您的订单已过期。

感谢您使用我们的平台。

请求发起自:${ip}

此致
${api_name}团队

", + "title": "您的P2P订单中检测到活动" + }, + "P2P_ORDER_CLOSED": { + "html": "

亲爱的 ${name}

在对您的申诉请求做出裁决后,您的订单已由我们的支持团队关闭

感谢您使用我们的平台。

此致
${api_name}团队

", + "title": "您的P2P订单中检测到活动" + }, + "P2P_BUYER_CANCELLED_ORDER": { + "html": "

亲爱的 ${name}

您已取消您的订单。交易已关闭。

感谢您使用我们的平台。

请求发起自:${ip}

此致
${api_name}团队

", + "title": "您的P2P订单中检测到活动" + }, + "P2P_BUYER_APPEALED_ORDER": { + "html": "

亲爱的 ${name}

您已对您的订单提出申诉。请联系支持以解决此问题。

请求发起自:${ip}

此致
${api_name}团队

", + "title": "您的P2P订单中检测到活动" + }, + "P2P_VENDOR_CONFIRMED_ORDER": { + "html": "

亲爱的 ${name}

您已确认交易并释放资金。

感谢您使用我们的平台。

请求发起自:${ip}

此致
${api_name}团队

", + "title": "您的P2P订单中检测到活动" + }, + "P2P_VENDOR_CANCELLED_ORDER": { + "html": "

亲爱的 ${name}

您已取消您的订单。交易已关闭。

感谢您使用我们的平台。

请求发起自:${ip}

此致
${api_name}团队

", + "title": "您的P2P订单中检测到活动" + }, + "P2P_VENDOR_APPEALED_ORDER": { + "html": "

亲爱的 ${name}

您已对您的订单提出申诉。请联系支持以解决此问题。

感谢您使用我们的平台。

请求发起自:${ip}

此致
${api_name}团队

", + "title": "您的P2P订单中检测到活动" } } } \ No newline at end of file diff --git a/server/mail/templates/index.js b/server/mail/templates/index.js index ea3eaecc4b..8e6174e932 100644 --- a/server/mail/templates/index.js +++ b/server/mail/templates/index.js @@ -320,7 +320,7 @@ const replaceHTMLContent = (type, html = '', email, data, language, domain) => { html = html.replace(/\$\{fee\}/g, data.fee || '0'); html = html.replace(/\$\{address\}/g, data.address || ''); html = html.replace(/\$\{ip\}/g, data.ip || ''); - html = html.replace(/\$\{link\}/g, data.confirmation_link || `${domain}/confirm-withdraw/${data.transaction_id}?currency=${data.currency}&amount=${data.amount}&address=${data.address}&fee=${data.fee}&network=${data.network}`); + html = html.replace(/\$\{link\}/g, data.confirmation_link || `${domain}/confirm-withdraw/${data.transaction_id}?currency=${data.currency}&amount=${data.amount}&address=${data.address}&fee=${data.fee}&fee_coin=${data.fee_coin}&network=${data.network}`); if(data.network) { html = html.replace(/\$\{network\}/g, data.network || ''); } else { @@ -434,6 +434,22 @@ const replaceHTMLContent = (type, html = '', email, data, language, domain) => { html = html.replace(/\$\{name\}/g, email || ''); html = html.replace(/\$\{api_name\}/g, API_NAME() || ''); } + else if (type === MAILTYPE.P2P_MERCHANT_IN_PROGRESS || + type === MAILTYPE.P2P_MERCHANT_IN_PROGRESS || + type === MAILTYPE.P2P_BUYER_PAID_ORDER || + type === MAILTYPE.P2P_ORDER_EXPIRED || + type === MAILTYPE.P2P_ORDER_CLOSED || + type === MAILTYPE.P2P_BUYER_CANCELLED_ORDER || + type === MAILTYPE.P2P_BUYER_APPEALED_ORDER || + type === MAILTYPE.P2P_VENDOR_CONFIRMED_ORDER || + type === MAILTYPE.P2P_VENDOR_CANCELLED_ORDER || + type === MAILTYPE.P2P_VENDOR_APPEALED_ORDER + ) { + html = html.replace(/\$\{name\}/g, email || ''); + html = html.replace(/\$\{api_name\}/g, API_NAME() || ''); + html = html.replace(/\$\{ip\}/g, data.ip || ''); + html = html.replace(/\$\{link\}/g, `${domain}/p2p/order/${data.order_id}`); + } return html; }; diff --git a/server/messages.js b/server/messages.js index f0efba40a1..02ebaebf69 100644 --- a/server/messages.js +++ b/server/messages.js @@ -202,6 +202,7 @@ exports.REBALANCE_SYMBOL_MISSING = 'Rebalance symbol for hedge account is missin exports.DYNAMIC_BROKER_CREATE_ERROR = 'Cannot create a dynamic broker without required fields'; exports.DYNAMIC_BROKER_UNSUPPORTED = 'Selected exchange is not supported by your exchange plan'; exports.DYNAMIC_BROKER_EXCHANGE_PLAN_ERROR = 'Cannot create a dynamic broker with Basic plan'; +exports.REFERRAL_HISTORY_NOT_ACTIVE = 'Referral feature is not active on the exchange'; exports.SYMBOL_NOT_FOUND = 'Symbol not found'; exports.INVALID_TOKEN_TYPE = 'invalid token type'; exports.NO_AUTH_TOKEN = 'no auth token sent'; @@ -238,7 +239,7 @@ exports.TERMINATED_STAKE_POOL_INVALID_ACTION = 'Cannot modify terminated stake p exports.INVALID_STAKE_POOL_ACTION = 'Cannot modify the fields when the stake pool is not uninitialized'; exports.INVALID_ONBOARDING_ACTION = 'Onboarding cannot be active while the status is uninitialized'; exports.INVALID_TERMINATION_ACTION = 'Cannot terminated stake pool while it is not paused'; -exports.FUNDING_ACCOUNT_INSUFFICIENT_BALANCE = 'There is not enough balance in the funding account. You cannot settle this stake pool'; +exports.FUNDING_ACCOUNT_INSUFFICIENT_BALANCE = 'There is not enough balance in the funding account to create this p2p deal'; exports.STAKE_POOL_NOT_EXIST = 'Stake pool does not exist'; exports.STAKE_POOL_ACCEPT_USER_ERROR = 'Stake pool is not active for accepting users'; exports.STAKE_POOL_NOT_ACTIVE = 'Cannot stake in a pool that is not active'; @@ -251,6 +252,7 @@ exports.STAKE_POOL_NOT_ACTIVE_FOR_UNSTAKING_ONBOARDING = 'Stake pool is not acti exports.STAKE_POOL_NOT_ACTIVE_FOR_UNSTAKING_STATUS = 'Cannot unstake in a pool that is not active'; exports.UNSTAKE_PERIOD_ERROR = 'Cannot unstake, period is not over'; exports.STAKE_UNSUPPORTED_EXCHANGE_PLAN = 'Your current exchange plan does not support cefi staking feature'; +exports.REFERRAL_UNSUPPORTED_EXCHANGE_PLAN = 'Your current exchange plan does not supporte referral feature'; exports.NO_ORACLE_PRICE_FOUND = 'There is no price for asset for rewarding in Oracle'; exports.REWARD_CURRENCY_CANNOT_BE_SAME = 'Reward currency cannot be same as the main currency'; exports.CANNOT_CHANGE_ADMIN_EMAIL = 'Cannot change admin email'; diff --git a/server/package.json b/server/package.json index d2d2dbf2a8..0a6ead8b66 100644 --- a/server/package.json +++ b/server/package.json @@ -1,5 +1,5 @@ { - "version": "2.10.4", + "version": "2.11.0", "private": false, "description": "HollaEx Kit", "keywords": [ diff --git a/server/plugins/index.js b/server/plugins/index.js index 17e44ad491..b67bf6b1da 100644 --- a/server/plugins/index.js +++ b/server/plugins/index.js @@ -119,6 +119,12 @@ const startPluginProcess = async () => { }); pluginWorkerThread = childProcess; + + pluginWorkerThread.on("exit", (code) => { + if (code === 0) { + startPluginProcess(); + } + }); }; const installPlugin = async (plugin) => { diff --git a/server/plugins/job.js b/server/plugins/job.js index 17142c4e43..74e24fe74f 100644 --- a/server/plugins/job.js +++ b/server/plugins/job.js @@ -6,8 +6,10 @@ const { sendEmail } = require('../mail'); const { isNumber } = require('lodash'); const BigNumber = require('bignumber.js'); const moment = require('moment'); + const { loggerPlugin } = require('../config/logger'); + const getTimezone = () => { const kitTimezone = toolsLib.getKitSecrets().emails.timezone; return isNumber(validTimezones[kitTimezone]) ? kitTimezone : 'Etc/UTC'; @@ -167,6 +169,7 @@ const unstakingCheckRunner = () => { err.message ); } + }, { scheduled: true, timezone: getTimezone() @@ -248,10 +251,47 @@ const updateRewardsCheckRunner = () => { }); } +const referralTradesRunner = () =>{ + cron.schedule('0 */4 * * *', async () => { + loggerPlugin.verbose( + '/plugins referralTradesRunner start' + ); + try { + const statusModel = toolsLib.database.getModel('status'); + const status = await statusModel.findOne({}); + if (!status?.kit?.referral_history_config?.active) return; + + const currentTime = moment().seconds(0).milliseconds(0).toISOString(); + await toolsLib.user.createUnrealizedReferralFees(currentTime); + + } catch (err) { + const adminAccount = await toolsLib.user.getUserByKitId(1); + sendEmail( + MAILTYPE.ALERT, + adminAccount.email, + { + type: 'Error during referralTradesRunner process!', + data: err.message + }, + adminAccount.settings + ); + loggerPlugin.error( + '/plugins referralTradesRunner error:', + err.message + ); + } + }, { + scheduled: true, + timezone: getTimezone() + }); +} + unstakingCheckRunner(); updateRewardsCheckRunner(); +referralTradesRunner(); module.exports = { unstakingCheckRunner, - updateRewardsCheckRunner + updateRewardsCheckRunner, + referralTradesRunner } \ No newline at end of file diff --git a/server/plugins/plugin-process.js b/server/plugins/plugin-process.js index 39399fd6d0..a7dc3c74e7 100644 --- a/server/plugins/plugin-process.js +++ b/server/plugins/plugin-process.js @@ -85,6 +85,7 @@ const initPluginProcess = async ({ PORT }) => { app, toolsLib, lodash, + restartPluginProcess, expressValidator, loggerPlugin, multer, @@ -164,6 +165,10 @@ const initPluginProcess = async ({ PORT }) => { } }; +const restartPluginProcess = () => { + process.exit(0); +} + if (!isMainThread) { loggerPlugin.verbose( 'plugins/plugin-process', diff --git a/server/tools/dbs/checkConfig.js b/server/tools/dbs/checkConfig.js index b0faea3d5c..8d5ca7d7bd 100644 --- a/server/tools/dbs/checkConfig.js +++ b/server/tools/dbs/checkConfig.js @@ -61,7 +61,10 @@ Status.findOne() quote: 'xht', spread: 0 }, + referral_history_config: existingKitConfigurations.referral_history_config || {}, coin_customizations: existingKitConfigurations.coin_customizations || {}, + balance_history_config: existingKitConfigurations.balance_history_config || {}, + p2p_config: existingKitConfigurations.p2p_config || {}, fiat_fees: existingKitConfigurations.fiat_fees || {}, balance_history_config: existingKitConfigurations.balance_history_config || {} }; @@ -112,4 +115,4 @@ Status.findOne() .catch((err) => { console.error('tools/dbs/checkConfig err', err); process.exit(1); - }); + }); \ No newline at end of file diff --git a/server/tools/nginx-dev/logs/error.log b/server/tools/nginx-dev/logs/error.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/server/tools/nginx-dev/logs/hollaex.access.log b/server/tools/nginx-dev/logs/hollaex.access.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/server/utils/hollaex-tools-lib/tools/common.js b/server/utils/hollaex-tools-lib/tools/common.js index 8eeed97b7d..8a1ab99f8b 100644 --- a/server/utils/hollaex-tools-lib/tools/common.js +++ b/server/utils/hollaex-tools-lib/tools/common.js @@ -25,7 +25,8 @@ const { VALID_USER_META_TYPES, DOMAIN, DEFAULT_FEES, - BALANCE_HISTORY_SUPPORTED_PLANS + BALANCE_HISTORY_SUPPORTED_PLANS, + REFERRAL_HISTORY_SUPPORTED_PLANS, } = require(`${SERVER_PATH}/constants`); const { COMMUNICATOR_CANNOT_UPDATE, @@ -33,7 +34,7 @@ const { SUPPORT_DISABLED, NO_NEW_DATA } = require(`${SERVER_PATH}/messages`); -const { each, difference, isPlainObject, isString, pick, isNil, omit, isNumber } = require('lodash'); +const { each, difference, isPlainObject, isString, pick, isNil, omit, isNumber, isInteger, isDate } = require('lodash'); const { publisher } = require('./database/redis'); const { sendEmail: sendSmtpEmail } = require(`${SERVER_PATH}/mail`); const { sendSMTPEmail: nodemailerEmail } = require(`${SERVER_PATH}/mail/utils`); @@ -352,6 +353,103 @@ const joinKitConfig = (existingKitConfig = {}, newKitConfig = {}) => { } } + if (newKitConfig.p2p_config) { + + const exchangeInfo = getKitConfig().info; + + if (!BALANCE_HISTORY_SUPPORTED_PLANS.includes(exchangeInfo.plan)) + throw new Error('Exchange plan does not support this feature'); + + if (newKitConfig.p2p_config.enable == null) { + throw new Error('enable cannot be null'); + } + if (newKitConfig.p2p_config.bank_payment_methods == null) { + throw new Error('bank_payment_methods cannot be null'); + } + if (newKitConfig.p2p_config.starting_merchant_tier == null) { + throw new Error('starting_merchant_tier cannot be null'); + } + if (newKitConfig.p2p_config.starting_user_tier == null) { + throw new Error('starting_user_tier cannot be null'); + } + if (newKitConfig.p2p_config.digital_currencies == null) { + throw new Error('digital_currencies cannot be null'); + } + if (newKitConfig.p2p_config.fiat_currencies == null) { + throw new Error('fiat_currencies cannot be null'); + } + if (newKitConfig.p2p_config.side == null) { + throw new Error('side cannot be null'); + } + if (newKitConfig.p2p_config.source_account == null) { + throw new Error('source_account cannot be null'); + } + if (newKitConfig.p2p_config.merchant_fee == null) { + throw new Error('merchant_fee cannot be null'); + } + if (newKitConfig.p2p_config.user_fee == null) { + throw new Error('buyer_fee cannot be null'); + } + } + + if (newKitConfig.referral_history_config) { + const exchangeInfo = getKitConfig().info; + + if (!REFERRAL_HISTORY_SUPPORTED_PLANS.includes(exchangeInfo.plan)) { + throw new Error('Exchange plan does not support this feature'); + } + + if (!newKitConfig.referral_history_config.hasOwnProperty('active')) { + throw new Error('active key does not exist'); + } + + if (!newKitConfig.referral_history_config.hasOwnProperty('currency')) { + throw new Error('currency key does not exist'); + } + + if (existingKitConfig?.referral_history_config?.currency && existingKitConfig?.referral_history_config?.currency !== newKitConfig?.referral_history_config?.currency) { + throw new Error('currency cannot be changed'); + } + + if (!newKitConfig.referral_history_config.hasOwnProperty('earning_rate')) { + throw new Error('earning_rate key does not exist'); + } + + if (!newKitConfig.referral_history_config.hasOwnProperty('minimum_amount')) { + throw new Error('minimum amount key does not exist'); + } + + if (!newKitConfig.referral_history_config.hasOwnProperty('earning_period')) { + throw new Error('earning_period key does not exist'); + } + + if (!newKitConfig.referral_history_config.hasOwnProperty('distributor_id')) { + throw new Error('distributor_id key does not exist'); + } + + if (!existingKitConfig?.referral_history_config?.date_enabled && !newKitConfig.referral_history_config.hasOwnProperty('date_enabled')) { + newKitConfig.referral_history_config.date_enabled = new Date(); + } + + if (!isNumber( newKitConfig?.referral_history_config?.earning_rate)) { + throw new Error('Earning rate with data type number required for plugin'); + } else if ( newKitConfig?.referral_history_config?.earning_rate < 1 || newKitConfig?.referral_history_config?.earning_rate > 100) { + throw new Error('Earning rate must be within the range of 1 ~ 100'); + } else if (!isNumber( newKitConfig?.referral_history_config?.minimum_amount)) { + throw new Error('Minimum amount must be integer'); + } else if ( newKitConfig?.referral_history_config?.minimum_amount < 0 ) { + throw new Error('Minimum amount must be bigger than 0'); + } else if ( newKitConfig?.referral_history_config?.earning_rate % 10 !== 0) { + throw new Error('Earning rate must be in increments of 10'); + } else if (!isNumber(newKitConfig?.referral_history_config?.earning_period)) { + throw new Error('Earning period with data type number required for plugin'); + } else if ((!isInteger(newKitConfig?.referral_history_config?.earning_period) || newKitConfig?.referral_history_config?.earning_period < 0)) { + throw new Error('Earning period must be an integer greater than 0'); + } else if (!isNumber(newKitConfig?.referral_history_config?.distributor_id)) { + throw new Error('Distributor ID required for plugin'); + } + } + const joinedKitConfig = {}; KIT_CONFIG_KEYS.forEach((key) => { @@ -991,4 +1089,4 @@ module.exports = { parseNumber, getQuickTradePairs, getTransactionLimits -}; +}; \ No newline at end of file diff --git a/server/utils/hollaex-tools-lib/tools/index.js b/server/utils/hollaex-tools-lib/tools/index.js index c9119d5a63..9a0fea4bf0 100644 --- a/server/utils/hollaex-tools-lib/tools/index.js +++ b/server/utils/hollaex-tools-lib/tools/index.js @@ -13,6 +13,7 @@ const pair = require('./pair'); const exchange = require('./exchange'); const broker = require('./broker'); const stake = require('./stake'); +const p2p = require('./p2p'); module.exports = { ...common, @@ -27,5 +28,6 @@ module.exports = { pair, exchange, broker, - stake + stake, + p2p }; diff --git a/server/utils/hollaex-tools-lib/tools/p2p.js b/server/utils/hollaex-tools-lib/tools/p2p.js new file mode 100644 index 0000000000..dd13c8ec47 --- /dev/null +++ b/server/utils/hollaex-tools-lib/tools/p2p.js @@ -0,0 +1,1116 @@ +'use strict'; + +const { getModel } = require('./database/model'); +const { SERVER_PATH } = require('../constants'); +const { getNodeLib } = require(`${SERVER_PATH}/init`); +const { P2P_SUPPORTED_PLANS } = require(`${SERVER_PATH}/constants`); +const { getUserByKitId } = require('./user'); +const { subscribedToCoin, getKitConfig } = require('./common'); +const { transferAssetByKitIds, getUserBalanceByKitId } = require('./wallet'); +const { Op } = require('sequelize'); +const BigNumber = require('bignumber.js'); +const { getKitCoin } = require('./common'); +const { paginationQuery, timeframeQuery, orderingQuery } = require('./database/helpers'); +const dbQuery = require('./database/query'); +const uuid = require('uuid/v4'); +const { parse } = require('json2csv'); +const moment = require('moment'); +const { sendEmail } = require('../../../mail'); +const { MAILTYPE } = require('../../../mail/strings'); + +const { + NO_DATA_FOR_CSV, + FUNDING_ACCOUNT_INSUFFICIENT_BALANCE, + USER_NOT_FOUND, +} = require(`${SERVER_PATH}/messages`); + + +const fetchP2PDisputes = async (opts = { + user_id: null, + limit: null, + page: null, + order_by: null, + order: null, + start_date: null, + end_date: null, + format: null +}) => { + const pagination = paginationQuery(opts.limit, opts.page); + const ordering = orderingQuery(opts.order_by, opts.order); + const timeframe = timeframeQuery(opts.start_date, opts.end_date); + + const query = { + where: { + created_at: timeframe, + ...(opts.user_id && { initiator_id: opts.user_id }), + + }, + order: [ordering], + ...(!opts.format && pagination), + }; + + if (opts.format) { + return dbQuery.fetchAllRecords('p2pDispute', query) + .then((data) => { + if (opts.format && opts.format === 'csv') { + if (data.data.length === 0) { + throw new Error(NO_DATA_FOR_CSV); + } + const csv = parse(data.data, Object.keys(data.data[0])); + return csv; + } else { + return data; + } + }); + } else { + return dbQuery.findAndCountAllWithRows('p2pDispute', query); + } +}; + +const fetchP2PDeals = async (opts = { + user_id: null, + status: null, + limit: null, + page: null, + order_by: null, + order: null, + start_date: null, + end_date: null, + format: null +}) => { + const pagination = paginationQuery(opts.limit, opts.page); + const ordering = orderingQuery(opts.order_by, opts.order); + const timeframe = timeframeQuery(opts.start_date, opts.end_date); + + const query = { + where: { + created_at: timeframe, + ...(opts.user_id && { merchant_id: opts.user_id }), + ...(opts.status && { status: opts.status }), + + }, + order: [ordering], + ...(!opts.format && pagination), + include: [ + { + model: getModel('user'), + as: 'merchant', + attributes: ['id', 'full_name'] + } + ] + }; + + if (opts.format) { + return dbQuery.fetchAllRecords('p2pDeal', query) + .then((data) => { + if (opts.format && opts.format === 'csv') { + if (data.data.length === 0) { + throw new Error(NO_DATA_FOR_CSV); + } + const csv = parse(data.data, Object.keys(data.data[0])); + return csv; + } else { + return data; + } + }); + } else { + return dbQuery.findAndCountAllWithRows('p2pDeal', query); + } +}; + +const fetchP2PTransactions = async (user_id, opts = { + id: null, + limit: null, + page: null, + order_by: null, + order: null, + start_date: null, + end_date: null, + format: null +}) => { + + const pagination = paginationQuery(opts.limit, opts.page); + const ordering = orderingQuery(opts.order_by, opts.order); + const timeframe = timeframeQuery(opts.start_date, opts.end_date); + + const query = { + where: { + created_at: timeframe, + ...(opts.id && { id: opts.id }), + [Op.or]: [ + { merchant_id: user_id }, + { user_id }, + ] + + }, + order: [ordering], + ...(!opts.format && pagination), + include: [ + { + model: getModel('p2pDeal'), + as: 'deal', + }, + { + model: getModel('user'), + as: 'merchant', + attributes: ['id', 'full_name'] + }, + { + model: getModel('user'), + as: 'buyer', + attributes: ['id', 'full_name'] + }, + ] + }; + + if (opts.format) { + return dbQuery.fetchAllRecords('p2pTransaction', query) + .then((data) => { + if (opts.format && opts.format === 'csv') { + if (data.data.length === 0) { + throw new Error(NO_DATA_FOR_CSV); + } + const csv = parse(data.data, Object.keys(data.data[0])); + return csv; + } else { + return data; + } + }); + } else { + return dbQuery.findAndCountAllWithRows('p2pTransaction', query); + } +}; + +const getP2PAccountBalance = async (account_id, coin) => { + + const balance = await getUserBalanceByKitId(account_id); + let symbols = {}; + + for (const key of Object.keys(balance)) { + if (key.includes('available') && balance[key]) { + let symbol = key?.split('_')?.[0]; + symbols[symbol] = balance[key]; + } + } + + return symbols[coin]; +}; + +const createP2PDeal = async (data) => { + let { + merchant_id, + side, + buying_asset, + spending_asset, + margin, + total_order_amount, + min_order_value, + max_order_value, + } = data; + + const exchangeInfo = getKitConfig().info; + + if(!P2P_SUPPORTED_PLANS.includes(exchangeInfo.plan)) { + throw new Error('Service not supported by your exchange plan'); + } + + const p2pConfig = getKitConfig()?.p2p_config; + + const merchant = await getUserByKitId(merchant_id); + + //Check Merhcant Tier + if (p2pConfig.starting_merchant_tier > merchant.verification_level) { + throw new Error('Your tier does not support creating P2P deals'); + } + + if (!subscribedToCoin(spending_asset)) { + throw new Error('Invalid coin ' + spending_asset); + } + + if (!subscribedToCoin(buying_asset)) { + throw new Error('Invalid coin ' + buying_asset); + } + + const balance = await getP2PAccountBalance(merchant_id, buying_asset); + + if(new BigNumber(balance).comparedTo(new BigNumber(total_order_amount)) !== 1) { + throw new Error(FUNDING_ACCOUNT_INSUFFICIENT_BALANCE); + } + if(min_order_value < 0) { + throw new Error('cannot be less than 0'); + } + + if(max_order_value < 0) { + throw new Error('max order value cannot be less than 0'); + } + + if(min_order_value > max_order_value) { + throw new Error('min order value cannot be bigger than max'); + } + + if (margin < 0) { + throw new Error('Margin cannot be less than 0'); + } + + if (side !== 'sell') { + throw new Error('side can only be sell'); + } + + data.status = true; + + return getModel('p2pDeal').create(data, { + fields: [ + 'merchant_id', + 'side', + 'price_type', + 'buying_asset', + 'spending_asset', + 'exchange_rate', + 'spread', + 'total_order_amount', + 'min_order_value', + 'max_order_value', + 'terms', + 'auto_response', + 'payment_methods', + 'status', + 'region' + ] + }); +}; + +const updateP2PDeal = async (data) => { + let { + id, + edited_ids, + merchant_id, + side, + buying_asset, + spending_asset, + margin, + total_order_amount, + min_order_value, + max_order_value, + payment_method_used, + status, + } = data; + + const exchangeInfo = getKitConfig().info; + + if (!P2P_SUPPORTED_PLANS.includes(exchangeInfo.plan)) { + throw new Error('Service not supported by your exchange plan'); + } + + if (edited_ids != null) { + const deals = await getModel('p2pDeal').findAll({ + where: { + id: edited_ids + } + }); + + deals.forEach(deal => { + if (deal.merchant_id !== merchant_id) { + throw new Error('Merchant id is not the same'); + } + }); + await getModel('p2pDeal').update({ status }, { where : { id : edited_ids }}); + return { message : 'success' }; + } + + const p2pDeal = await getModel('p2pDeal').findOne({ where: { id } }); + if (!p2pDeal) { + throw new Error('deal does not exist'); + } + + if(p2pDeal.merchant_id !== merchant_id) { + throw new Error('Merchant Id is not the same'); + } + + //Check Merhcant Tier + + if (!subscribedToCoin(spending_asset)) { + throw new Error('Invalid coin ' + spending_asset); + } + + if (!subscribedToCoin(buying_asset)) { + throw new Error('Invalid coin ' + buying_asset); + } + + const balance = await getP2PAccountBalance(merchant_id, buying_asset); + + if(new BigNumber(balance).comparedTo(new BigNumber(total_order_amount)) !== 1) { + throw new Error(FUNDING_ACCOUNT_INSUFFICIENT_BALANCE); + } + if(min_order_value < 0) { + throw new Error('cannot be less than 0'); + } + + if(max_order_value < 0) { + throw new Error('cannot be less than 0'); + } + + if(min_order_value > max_order_value) { + throw new Error('cannot be bigger'); + } + + if (margin < 0) { + throw new Error('Margin cannot be less than 0'); + } + + if (side !== 'sell') { + throw new Error('side can only be sell'); + } + + if (data.status == null) { + data.status = true; + } + + return p2pDeal.update(data, { + fields: [ + 'merchant_id', + 'side', + 'price_type', + 'buying_asset', + 'spending_asset', + 'exchange_rate', + 'spread', + 'total_order_amount', + 'min_order_value', + 'max_order_value', + 'terms', + 'auto_response', + 'payment_methods', + 'status', + 'region' + ] + }); +}; + +const createP2PTransaction = async (data) => { + let { + deal_id, + user_id, + amount_fiat, + payment_method_used, + ip + } = data; + + const exchangeInfo = getKitConfig().info; + + if(!P2P_SUPPORTED_PLANS.includes(exchangeInfo.plan)) { + throw new Error('Service not supported by your exchange plan'); + } + + // Check User tier + const p2pConfig = getKitConfig()?.p2p_config; + + const p2pDeal = await getModel('p2pDeal').findOne({ where: { id: deal_id } }); + + const { max_order_value, min_order_value, exchange_rate, spread } = p2pDeal; + const { merchant_id } = p2pDeal; + + if (!p2pDeal) { + throw new Error('deal does not exist'); + } + + if (!p2pDeal.status) { + throw new Error('deal is not active'); + } + + const buyer = await getUserByKitId(user_id); + + if (!buyer) { + throw new Error(USER_NOT_FOUND); + } + + //Check Buyer Tier + if (p2pConfig.starting_user_tier > buyer.verification_level) { + throw new Error('Your tier does not support creating P2P transactions'); + } + + + if (merchant_id === user_id) { + throw new Error('Merchant and Buyer cannot be same'); + } + + //Cant have more than 3 active transactions per user + const userTransactions = await getModel('p2pTransaction').findAll({ where: { user_id: buyer.id, transaction_status: "active" } }); + + if (userTransactions.length > 3) { + throw new Error('You have currently 3 active order, please complete them before creating another one'); + } + + const merchant = await getUserByKitId(p2pDeal.merchant_id); + + const merchantBalance = await getP2PAccountBalance(merchant_id, p2pDeal.buying_asset); + + const price = new BigNumber(exchange_rate).multipliedBy(1 + (spread / 100)); + const amount_digital_currency = new BigNumber(amount_fiat).dividedBy(price).toNumber(); + + const merchantFeeAmount = (new BigNumber(amount_digital_currency).multipliedBy(p2pConfig.merchant_fee)) + .dividedBy(100).toNumber(); + + const buyerFeeAmount = (new BigNumber(amount_digital_currency).multipliedBy(p2pConfig.user_fee)) + .dividedBy(100).toNumber(); + + + if (new BigNumber(merchantBalance).comparedTo(new BigNumber(amount_digital_currency + merchantFeeAmount + buyerFeeAmount)) !== 1) { + throw new Error('Transaction is not possible at the moment'); + } + + if (new BigNumber(amount_fiat).comparedTo(new BigNumber(max_order_value)) === 1) { + throw new Error('input amount cannot be bigger than max allowable order amount'); + } + + if (new BigNumber(amount_fiat).comparedTo(new BigNumber(min_order_value)) === -1) { + throw new Error('input amount cannot be lower than min allowable order amount'); + } + + //Check the payment method + const hasMethod = p2pDeal.payment_methods.find(method => method.system_name === payment_method_used.system_name); + + if (!hasMethod) { + throw new Error('invalid payment method'); + } + + const coinConfiguration = getKitCoin(p2pDeal.buying_asset); + const { increment_unit } = coinConfiguration; + + const decimalPoint = new BigNumber(increment_unit).dp(); + const amount = new BigNumber(amount_digital_currency).decimalPlaces(decimalPoint, BigNumber.ROUND_DOWN).toNumber(); + + data.user_status = 'pending'; + data.merchant_status = 'pending'; + data.transaction_status = 'active'; + data.transaction_duration = 30; + data.transaction_id = uuid(); + data.merchant_id = merchant_id; + data.user_id = user_id; + data.amount_digital_currency = amount; + data.deal_id = deal_id; + const lock = await getNodeLib().lockBalance(merchant.network_id, p2pDeal.buying_asset, amount_digital_currency + merchantFeeAmount + buyerFeeAmount); + data.locked_asset_id = lock.id; + data.price = price.toNumber(); + + const firstChatMessage = { + sender_id: merchant_id, + receiver_id: user_id, + message: p2pDeal.auto_response, + type: 'message', + created_at: new Date() + }; + + data.messages = [firstChatMessage]; + + const transaction = await getModel('p2pTransaction').create(data, { + fields: [ + 'deal_id', + 'transaction_id', + 'locked_asset_id', + 'merchant_id', + 'user_id', + 'amount_digital_currency', + 'amount_fiat', + 'payment_method_used', + 'user_status', + 'merchant_status', + 'cancellation_reason', + 'transaction_expired', + 'transaction_timestamp', + 'merchant_release', + 'transaction_duration', + 'transaction_status', + 'price', + 'messages' + ] + }); + + sendEmail( + MAILTYPE.P2P_MERCHANT_IN_PROGRESS, + merchant.email, + { + order_id: transaction.id, + ip + }, + merchant.settings + ); + + return transaction; +}; + +const updateP2pTransaction = async (data) => { + let { + user_id, + id, + user_status, + merchant_status, + cancellation_reason, + ip + } = data; + + const transaction = await getModel('p2pTransaction').findOne({ where: { id } }); + const p2pDeal = await getModel('p2pDeal').findOne({ where: { id: transaction.deal_id } }); + const merchant = await getUserByKitId(p2pDeal.merchant_id); + const p2pConfig = getKitConfig()?.p2p_config; + const user = await getUserByKitId(user_id); + + // eslint-disable-next-line no-prototype-builtins + if (user_id === transaction.merchant_id && data.hasOwnProperty(user_status)) { + throw new Error('merchant cannot update buyer status'); + } + // eslint-disable-next-line no-prototype-builtins + if (user_id === transaction.user_id && data.hasOwnProperty(merchant_status)) { + throw new Error('buyer cannot update merchant status'); + } + + if (user_id !== transaction.merchant_id && user_id !== transaction.user_id) { + throw new Error('you cannot update this transaction'); + } + + if (!transaction) { + throw new Error('transaction does not exist'); + } + + if (transaction.transaction_status === 'expired') { + throw new Error(`Transaction expired, ${transaction.transaction_duration} minutes passed without any action`); + } + + if (transaction.merchant_status !== 'pending' && moment() > moment(transaction.created_at).add(transaction.transaction_duration || 30 ,'minutes')) { + + if (transaction.transaction_status !== 'expired') { + + const newMessages = [...transaction.messages]; + + const chatMessage = { + sender_id: user_id, + receiver_id: merchant.id, + message: 'ORDER_EXPIRED', + type: 'notification', + created_at: new Date() + }; + + sendEmail( + MAILTYPE.P2P_ORDER_EXPIRED, + user.email, + { + order_id: id, + ip + }, + user.settings + ); + + newMessages.push(chatMessage); + + await transaction.update({ transaction_status: 'expired', messages: newMessages }, { + fields: [ + 'transaction_status', + 'messages' + ] + }); + + } + + throw new Error(`Transaction expired, ${transaction.transaction_duration} minutes passed without any action`); + } + + + if (transaction.transaction_status !== 'active') { + throw new Error('Cannot update inactive transaction'); + } + if (transaction.merchant_status === 'confirmed' && transaction.user_status === 'confirmed') { + throw new Error('Cannot update complete transaction'); + } + + if (merchant_status === 'confirmed' && transaction.user_status !== 'confirmed') { + throw new Error('merchant cannot confirm the transaction while buyer not confirmed'); + } + + if (merchant_status === 'confirmed' && transaction.user_status === 'confirmed') { + await getNodeLib().unlockBalance(merchant.network_id, transaction.locked_asset_id); + + const merchantFeeAmount = (new BigNumber(transaction.amount_digital_currency).multipliedBy(p2pConfig.merchant_fee)) + .dividedBy(100).toNumber(); + + const buyerFeeAmount = (new BigNumber(transaction.amount_digital_currency).multipliedBy(p2pConfig.user_fee)) + .dividedBy(100).toNumber(); + const buyerTotalAmount = new BigNumber(transaction.amount_digital_currency).minus(new BigNumber(buyerFeeAmount)).toNumber(); + await transferAssetByKitIds(merchant.id, transaction.user_id, p2pDeal.buying_asset, buyerTotalAmount, 'P2P Transaction', false, { category: 'p2p' }); + + //send the fees to the source account + if (p2pConfig.source_account !== merchant.id) { + const totalFees = (new BigNumber(merchantFeeAmount).plus(buyerFeeAmount)).toNumber(); + await transferAssetByKitIds(merchant.id, p2pConfig.source_account, p2pDeal.buying_asset, totalFees, 'P2P Transaction', false, { category: 'p2p' }); + } + + data.transaction_status = 'complete'; + data.merchant_release = new Date(); + } + + if (user_status === 'appeal' || merchant_status === 'appeal') { + let initiator_id; + let defendant_id; + if (user_status === 'appeal') { + initiator_id = transaction.user_id; + defendant_id = transaction.merchant_id; + } else { + initiator_id = transaction.merchant_id; + defendant_id = transaction.user_id; + } + await getNodeLib().unlockBalance(merchant.network_id, transaction.locked_asset_id); + await createP2pDispute({ + transaction_id: transaction.id, + initiator_id, + defendant_id, + reason: cancellation_reason || '', + }); + data.transaction_status = 'appealed'; + } + + if (user_status === 'cancelled' || merchant_status === 'cancelled') { + await getNodeLib().unlockBalance(merchant.network_id, transaction.locked_asset_id); + data.transaction_status = 'cancelled'; + } + + const newMessages = [...transaction.messages]; + + if (user_status === 'confirmed') { + const chatMessage = { + sender_id: user_id, + receiver_id: merchant.id, + message: 'BUYER_PAID_ORDER', + type: 'notification', + created_at: new Date() + }; + + sendEmail( + MAILTYPE.P2P_BUYER_PAID_ORDER, + user.email, + { + order_id: id, + ip + }, + user.settings + ); + + newMessages.push(chatMessage); + } + + if (user_status === 'cancelled') { + const chatMessage = { + sender_id: user_id, + receiver_id: merchant.id, + message: 'BUYER_CANCELLED_ORDER', + type: 'notification', + created_at: new Date() + }; + + sendEmail( + MAILTYPE.P2P_BUYER_CANCELLED_ORDER, + user.email, + { + order_id: id, + ip + }, + user.settings + ); + + newMessages.push(chatMessage); + } + + if (user_status === 'appeal') { + const chatMessage = { + sender_id: user_id, + receiver_id: merchant.id, + message: 'BUYER_APPEALED_ORDER', + type: 'notification', + created_at: new Date() + }; + + sendEmail( + MAILTYPE.P2P_BUYER_APPEALED_ORDER, + user.email, + { + order_id: id, + ip + }, + user.settings + ); + + newMessages.push(chatMessage); + } + + + if (merchant_status === 'confirmed') { + const chatMessage = { + sender_id: merchant.id, + receiver_id: transaction.user_id, + message: 'VENDOR_CONFIRMED_ORDER', + type: 'notification', + created_at: new Date() + }; + + sendEmail( + MAILTYPE.P2P_VENDOR_CONFIRMED_ORDER, + user.email, + { + order_id: id, + ip + }, + user.settings + ); + + newMessages.push(chatMessage); + } + + if (merchant_status === 'cancelled') { + const chatMessage = { + sender_id: merchant.id, + receiver_id: transaction.user_id, + message: 'VENDOR_CANCELLED_ORDER', + type: 'notification', + created_at: new Date() + }; + + sendEmail( + MAILTYPE.P2P_VENDOR_CANCELLED_ORDER, + user.email, + { + order_id: id, + ip + }, + user.settings + ); + + newMessages.push(chatMessage); + } + + if (merchant_status === 'appeal') { + const chatMessage = { + sender_id: merchant.id, + receiver_id: transaction.user_id, + message: 'VENDOR_APPEALED_ORDER', + type: 'notification', + created_at: new Date() + }; + + sendEmail( + MAILTYPE.P2P_VENDOR_APPEALED_ORDER, + user.email, + { + order_id: id, + ip + }, + user.settings + ); + + newMessages.push(chatMessage); + } + + return transaction.update({...data, messages: newMessages}, { + fields: [ + 'user_status', + 'merchant_status', + 'cancellation_reason', + 'transaction_expired', + 'transaction_timestamp', + 'merchant_release', + 'transaction_duration', + 'transaction_status', + 'messages' + ] + }); +}; + + +const createP2pDispute = async (data) => { + data.status = true; + return getModel('p2pDispute').create(data, { + fields: [ + 'transaction_id', + 'initiator_id', + 'defendant_id', + 'reason', + 'resolution', + 'status', + ] + }); +}; + +const updateP2pDispute = async (data) => { + const p2pDispute = await getModel('p2pDispute').findOne({ where: { id: data.id } }); + + if (!p2pDispute) { + throw new Error('no record found'); + }; + + const dispute = await p2pDispute.update(data, { + fields: [ + 'resolution', + 'status' + ] + }); + + if (data.status == false) { + const transaction = await getModel('p2pTransaction').findOne({ where: { id: dispute.transaction_id } }); + await transaction.update({ + transaction_status: 'closed' + }, { fields: ['transaction_status']}); + + + const chatMessage = { + sender_id: transaction.user_id, + receiver_id: transaction.merchant_id, + message: 'ORDER_CLOSED', + type: 'notification', + created_at: new Date() + }; + + const merchant = await getUserByKitId(transaction.merchant_id); + const user = await getUserByKitId(transaction.user_id); + + sendEmail( + MAILTYPE.P2P_ORDER_CLOSED, + user.email, + { + order_id: transaction.id, + }, + user.settings + ); + + sendEmail( + MAILTYPE.P2P_ORDER_CLOSED, + merchant.email, + { + order_id: transaction.id, + }, + merchant.settings + ); + + const newMessages = [...transaction.messages]; + newMessages.push(chatMessage); + + transaction.update({ messages: newMessages }, { + fields: [ + 'messages' + ] + }); + + } + + return dispute; +}; + +const createP2pChatMessage = async (data) => { + const transaction = await getModel('p2pTransaction').findOne({ where: { id: data.transaction_id } }); + if (!transaction) { + throw new Error ('no transaction found'); + } + + if (data.sender_id !== transaction.merchant_id && data.sender_id !== transaction.user_id) { + throw new Error('unauthorized'); + } + + if (transaction.transaction_status !== 'active') { + throw new Error('Cannot message in inactive transaction'); + } + + const chatMessage = { + sender_id: data.sender_id, + receiver_id: data.receiver_id, + message: data.message, + type: 'message', + created_at: new Date() + }; + + const newMessages = [...transaction.messages]; + newMessages.push(chatMessage); + + // return transaction.update({ messages: fn('array_append', col('messages'), chatMessage) }, { + // fields: [ + // 'messages' + // ] + // }); + + return transaction.update({ messages: newMessages }, { + fields: [ + 'messages' + ] + }); +}; + +const updateMerchantProfile = async (data) => { + const p2pMerchant = await getModel('p2pMerchant').findOne({ id: data.id }); + + if(!p2pMerchant) { + return getModel('p2pMerchant').create(data, { + fields: [ + 'user_id', + 'blocked_users' + ] + }); + } else { + p2pMerchant.update(data, { + fields: [ + 'user_id', + 'blocked_users' + ] + }); + } +}; + +const createMerchantFeedback = async (data) => { + const transaction = await getModel('p2pTransaction').findOne({ where: { id: data.transaction_id } }); + + if (!transaction) { + throw new Error ('no transaction found'); + } + + if (transaction.user_id !== data.user_id) { + throw new Error ('unauthorized'); + } + + const foundFeedback = await getModel('P2pMerchantsFeedback').findOne({ where: { transaction_id: data.transaction_id } }); + + if (foundFeedback) { + throw new Error ('you already made a feedback'); + } + + if (data.rating > 5) { + throw new Error ('undefined rating'); + } + + if (data.rating < 1) { + throw new Error ('undefined rating'); + } + + data.merchant_id = transaction.merchant_id; + return getModel('P2pMerchantsFeedback').create(data, { + fields: [ + 'merchant_id', + 'user_id', + 'transaction_id', + 'rating', + 'comment', + ] + }); +}; + +const fetchP2PFeedbacks = async (opts = { + transaction_id: null, + merchant_id: null, + limit: null, + page: null, + order_by: null, + order: null, + start_date: null, + end_date: null, + format: null +}) => { + const pagination = paginationQuery(opts.limit, opts.page); + const ordering = orderingQuery(opts.order_by, opts.order); + const timeframe = timeframeQuery(opts.start_date, opts.end_date); + + const query = { + where: { + created_at: timeframe, + ...(opts.transaction_id && { transaction_id: opts.transaction_id }), + ...(opts.merchant_id && { merchant_id: opts.merchant_id }), + }, + order: [ordering], + ...(!opts.format && pagination), + include: [ + { + model: getModel('user'), + as: 'user', + attributes: ['id', 'full_name'] + }, + ] + }; + + if (opts.format) { + return dbQuery.fetchAllRecords('P2pMerchantsFeedback', query) + .then((data) => { + if (opts.format && opts.format === 'csv') { + if (data.data.length === 0) { + throw new Error(NO_DATA_FOR_CSV); + } + const csv = parse(data.data, Object.keys(data.data[0])); + return csv; + } else { + return data; + } + }); + } else { + return dbQuery.findAndCountAllWithRows('P2pMerchantsFeedback', query); + } +}; + +const fetchP2PProfile = async (user_id) => { + + const P2pTransaction = getModel('p2pTransaction'); + const P2pMerchantsFeedback = getModel('P2pMerchantsFeedback'); + + // Total Transactions per Merchant + const totalTransactions = await P2pTransaction.count({ + where: { merchant_id: user_id } + }); + + // Completion Rate of Transactions + const completedTransactions = await P2pTransaction.count({ + where: { + merchant_id: user_id, + transaction_status: 'complete' + } + }); + + const completionRate = (completedTransactions / totalTransactions) * 100; + + // Positive Feedback Percentage + const totalFeedbacks = await P2pMerchantsFeedback.count({ + where: { merchant_id: user_id } + }); + const positiveFeedbackCount = await P2pMerchantsFeedback.count({ + where: { + merchant_id: user_id, + rating: { + [Op.gte]: 3 + } + } + }); + + const negativeFeedbackCount = await P2pMerchantsFeedback.count({ + where: { + merchant_id: user_id, + rating: { + [Op.lte]: 2 + } + } + }); + + const positiveFeedbackRate = (positiveFeedbackCount / totalFeedbacks) * 100; + + return { + totalTransactions, + completionRate, + positiveFeedbackRate, + positiveFeedbackCount, + negativeFeedbackCount + } +}; + +module.exports = { + createP2PDeal, + createP2PTransaction, + createP2pDispute, + updateP2pTransaction, + updateP2pDispute, + updateMerchantProfile, + createMerchantFeedback, + createP2pChatMessage, + fetchP2PDeals, + fetchP2PTransactions, + fetchP2PDisputes, + updateP2PDeal, + fetchP2PFeedbacks, + fetchP2PProfile +}; \ No newline at end of file diff --git a/server/utils/hollaex-tools-lib/tools/user.js b/server/utils/hollaex-tools-lib/tools/user.js index df8dff5a45..4837da36d3 100644 --- a/server/utils/hollaex-tools-lib/tools/user.js +++ b/server/utils/hollaex-tools-lib/tools/user.js @@ -57,6 +57,8 @@ const { CANNOT_CHANGE_ADMIN_EMAIL, EMAIL_IS_SAME, EMAIL_EXISTS, + REFERRAL_HISTORY_NOT_ACTIVE, + REFERRAL_UNSUPPORTED_EXCHANGE_PLAN, CANNOT_CHANGE_DELETED_EMAIL, SERVICE_NOT_SUPPORTED, BALANCE_HISTORY_NOT_ACTIVE @@ -72,6 +74,7 @@ const { OMITTED_USER_FIELDS, DEFAULT_ORDER_RISK_PERCENTAGE, AFFILIATION_CODE_LENGTH, + REFERRAL_HISTORY_SUPPORTED_PLANS, LOGIN_TIME_OUT, TOKEN_TIME_LONG, TOKEN_TIME_NORMAL, @@ -81,18 +84,20 @@ const { } = require(`${SERVER_PATH}/constants`); const { sendEmail } = require(`${SERVER_PATH}/mail`); const { MAILTYPE } = require(`${SERVER_PATH}/mail/strings`); -const { getKitConfig, isValidTierLevel, getKitTier, isDatetime, getKitCoins } = require('./common'); +const { getKitConfig, isValidTierLevel, getKitTier, isDatetime, getKitSecrets, sendCustomEmail, emailHtmlBoilerplate, getDomain, updateKitConfigSecrets, sleep, getKitCoins } = require('./common'); const { isValidPassword, createSession } = require('./security'); const { getNodeLib } = require(`${SERVER_PATH}/init`); const { all, reject } = require('bluebird'); -const { Op } = require('sequelize'); -const { paginationQuery, timeframeQuery, orderingQuery } = require('./database/helpers'); +const { Op, fn, col, literal } = require('sequelize'); +const { paginationQuery, timeframeQuery, orderingQuery, convertSequelizeCountAndRows } = require('./database/helpers'); const { parse } = require('json2csv'); const flatten = require('flat'); const uuid = require('uuid/v4'); const { checkCaptcha, validatePassword, verifyOtpBeforeAction } = require('./security'); const geoip = require('geoip-lite'); const moment = require('moment'); +const mathjs = require('mathjs'); +const { loggerUser } = require('../../../config/logger'); const BigNumber = require('bignumber.js'); let networkIdToKitId = {}; @@ -480,39 +485,46 @@ const generateAffiliationCode = () => { }; const getUserByAffiliationCode = (affiliationCode) => { - const code = affiliationCode.toUpperCase().trim(); - return dbQuery.findOne('user', { - where: { affiliation_code: code }, - attributes: ['id', 'email', 'affiliation_code'] + const code = affiliationCode.trim(); + return dbQuery.findOne('referralCode', { + where: { code }, + attributes: ['id', 'user_id', 'discount', 'earning_rate'] }); }; const checkAffiliation = (affiliationCode, user_id) => { - // let discount = 0; // default discount rate in percentage return getUserByAffiliationCode(affiliationCode) .then((referrer) => { if (referrer) { - return getModel('affiliation').create({ + return all([getModel('affiliation').create({ user_id, - referer_id: referrer.id - }); + referer_id: referrer.user_id, + earning_rate: referrer.earning_rate, + code: affiliationCode, + }), + referrer, + getModel('referralCode').increment('referral_count', { by: 1, where: { id: referrer.id }}) + ]); } else { - return; + return []; + } + }) + .then(([affiliation, referrer]) => { + if (affiliation?.user_id) { + return getModel('user').update( + { + discount: referrer.discount + }, + { + where: { + id: affiliation.user_id + }, + fields: ['discount'] + } + ); } + return; }); - // .then((affiliation) => { - // return getModel('user').update( - // { - // discount - // }, - // { - // where: { - // id: affiliation.user_id - // }, - // fields: ['discount'] - // } - // ); - // }); }; const getAffiliationCount = (userId, opts = { @@ -2354,6 +2366,557 @@ const changeKitUserEmail = async (userId, newEmail, auditInfo) => { return updatedUser; }; +const getAllAffiliations = (query = {}) => { + return dbQuery.findAndCountAll('affiliation', query); +}; + +const applyEarningRate = (amount, earning_rate) => { + return mathjs.number( + mathjs.multiply( + mathjs.bignumber(amount), + mathjs.divide( + mathjs.bignumber(earning_rate), + mathjs.bignumber(100) + ) + ) + ); +}; + +const addAmounts = (amount1, amount2) => { + return mathjs.number( + mathjs.add( + mathjs.bignumber(amount1), + mathjs.bignumber(amount2) + ) + ); +}; + + +const getUserReferralCodes = async ( + opts = { + user_id: null, + limit: null, + page: null, + order_by: null, + order: null, + start_date: null, + end_date: null, + format: null + }) => { + + const pagination = paginationQuery(opts.limit, opts.page); + const ordering = orderingQuery(opts.order_by, opts.order); + const timeframe = timeframeQuery(opts.start_date, opts.end_date); + + const query = { + where: { + created_at: timeframe, + ...(opts.user_id && { user_id: opts.user_id }) + }, + order: [ordering], + ...(!opts.format && pagination), + }; + + if (opts.format) { + return dbQuery.fetchAllRecords('referralCode', query) + .then((codes) => { + if (opts.format && opts.format === 'csv') { + if (codes.data.length === 0) { + throw new Error(NO_DATA_FOR_CSV); + } + const csv = parse(codes.data, Object.keys(codes.data[0])); + return csv; + } else { + return codes; + } + }); + } else { + return dbQuery.findAndCountAllWithRows('referralCode', query); + } +}; + +const createUserReferralCode = async (data) => { + const { user_id, discount, earning_rate, code, is_admin } = data; + + const { + earning_rate: EARNING_RATE, + } = getKitConfig()?.referral_history_config || {}; + + if (discount < 0) { + throw new Error('discount cannot be negative'); + }; + + if (discount > 100) { + throw new Error('discount cannot be more than 100'); + }; + + if (discount % 10 !== 0) { + throw new Error('discount must be in increments of 10'); + }; + + if (earning_rate < 1) { + throw new Error('earning rate cannot be less than 1'); + }; + + if (earning_rate > 100) { + throw new Error('earning rate cannot be more than 100'); + }; + + if (earning_rate % 10 !== 0) { + throw new Error('earning rate must be in increments of 10'); + }; + + if (!is_admin && (earning_rate + discount > EARNING_RATE)) { + throw new Error('discount and earning rate combined cannot exceed exchange earning rate'); + }; + + if (!is_admin && code.length !== 6) { + throw new Error('invalid referral code'); + } + + const user = await getUserByKitId(user_id); + + if (!user) { + throw new Error(USER_NOT_FOUND); + }; + + if (!is_admin) { + const userReferralCodes = await getModel('referralCode').findAll({ + where: { + user_id + } + }) + if (userReferralCodes.length > 3) { + throw new Error('you cannot create more than 3 referral codes'); + } + }; + + const referralCode = await getModel('referralCode').create(data, { + fields: [ + 'user_id', + 'discount', + 'earning_rate', + 'code' + ] + }); + return referralCode; +}; + +const getUnrealizedReferral = async (user_id) => { + const exchangeInfo = getKitConfig().info; + + if (!REFERRAL_HISTORY_SUPPORTED_PLANS.includes(exchangeInfo.plan)) { + throw new Error(REFERRAL_UNSUPPORTED_EXCHANGE_PLAN); + } + + const { active } = getKitConfig()?.referral_history_config || {}; + if (!active) { + throw new Error(REFERRAL_HISTORY_NOT_ACTIVE); + } + + const referralHistoryModel = getModel('ReferralHistory'); + const unrealizedRecords = await referralHistoryModel.findAll({ + where: { referer: user_id, status: false }, + attributes: [ + 'referer', + [fn('sum', col('accumulated_fees')), 'accumulated_fees'], + ], + group: ['referer'], + }); + + return unrealizedRecords; +}; + +const getRealizedReferral = async (opts = { + user_id: null, + limit: null, + page: null, + order_by: null, + order: null, + start_date: null, + end_date: null, + format: null +}) => { + const pagination = paginationQuery(opts.limit, opts.page); + const ordering = orderingQuery(opts.order_by, opts.order); + const timeframe = timeframeQuery(opts.start_date, opts.end_date); + + const query = { + where: { + created_at: timeframe, + status: true, + ...(opts.user_id && { referer: opts.user_id }) + }, + order: [ordering], + ...(!opts.format && pagination), + }; + + if (opts.format) { + return dbQuery.fetchAllRecords('ReferralHistory', query) + .then((file) => { + if (opts.format && opts.format === 'csv') { + if (file.data.length === 0) { + throw new Error(NO_DATA_FOR_CSV); + } + const csv = parse(file.data, Object.keys(file.data[0])); + return csv; + } else { + return file; + } + }); + } else { + return dbQuery.findAndCountAllWithRows('ReferralHistory', query); + } +}; + +const createUnrealizedReferralFees = async (currentTime) => { + const { + earning_period: EARNING_PERIOD, + distributor_id: DISTRIBUTOR_ID, + date_enabled: DATE_ENABLED + } = getKitConfig()?.referral_history_config || {}; + + const exchangeInfo = getKitConfig().info; + + if (!REFERRAL_HISTORY_SUPPORTED_PLANS.includes(exchangeInfo.plan)) { + throw new Error(REFERRAL_UNSUPPORTED_EXCHANGE_PLAN); + } + + const { getAllTradesNetwork } = require('./order'); + const referralHistoryModel = getModel('ReferralHistory'); + + let userLastSettleDate = moment(DATE_ENABLED).toISOString(); + + const userLastTrade = await referralHistoryModel.findOne({ + order: [ [ 'last_settled', 'DESC' ]], + }); + + if (userLastTrade) { + userLastSettleDate = moment(userLastTrade.last_settled).toISOString(); + } + + return all([ + getUserByKitId(DISTRIBUTOR_ID, true, true), + getAllTradesNetwork( + null, + null, + null, + 'timestamp', + 'desc', + userLastSettleDate ? moment(userLastSettleDate).add(1, 'ms').toISOString() : null, + null, + 'all' + ) + ]) + .then(([distributor, { count, data: trades }]) => { + if (!distributor) { + throw new Error('No distributor found'); + } + + if (count === 0) { + throw new Error('No trades to settle'); + } + + const lastSettledTrade = trades[0].timestamp; + + const accumulatedFees = {}; + + for (let trade of trades) { + const { + maker_network_id, + taker_network_id, + maker_fee, + taker_fee, + maker_fee_coin, + taker_fee_coin + } = trade; + + if (maker_fee > 0 && maker_fee_coin) { + if (!accumulatedFees[maker_network_id]) { + accumulatedFees[maker_network_id] = {}; + } + + if (!isNumber(accumulatedFees[maker_network_id][maker_fee_coin])) { + accumulatedFees[maker_network_id][maker_fee_coin] = 0; + } + + accumulatedFees[maker_network_id][maker_fee_coin] = addAmounts( + accumulatedFees[maker_network_id][maker_fee_coin], + maker_fee + ); + } + + if (taker_fee > 0 && taker_fee_coin) { + if (!accumulatedFees[taker_network_id]) { + accumulatedFees[taker_network_id] = {}; + } + + if (!isNumber(accumulatedFees[taker_network_id][taker_fee_coin])) { + accumulatedFees[taker_network_id][taker_fee_coin] = 0; + } + + accumulatedFees[taker_network_id][taker_fee_coin] = addAmounts( + accumulatedFees[taker_network_id][taker_fee_coin], + taker_fee + ); + } + } + + const tradeUsers = Object.keys(accumulatedFees); + const tradeUsersAmount = tradeUsers.length; + + if (tradeUsersAmount === 0) { + throw new Error('No trades made with fees'); + } + + + return all([ + accumulatedFees, + lastSettledTrade, + getAllAffiliations({ + where: { + '$user.network_id$': tradeUsers, + ...(EARNING_PERIOD && { + created_at: { + [Op.gt]: moment(currentTime).subtract(EARNING_PERIOD, 'months').toISOString(), + [Op.lte]: currentTime + } + }) + }, + include: [ + { + model: getModel('user'), + as: 'user', + attributes: [ + 'id', + 'email', + 'network_id' + ] + }, + { + model: getModel('user'), + as: 'referer', + attributes: [ + 'id', + 'email', + 'network_id' + ] + } + ] + }) + ]); + }) + .then(async ([accumulatedFees, lastSettledTrade, { count, rows: affiliations }]) => { + const filteredFees = {}; + const referralHistory = []; + if (count === 0) { + throw new Error('No trades made by affiliated users'); + } + + for (let affiliation of affiliations) { + const refereeUser = affiliation.user; + const referer = affiliation.referer; + + if (accumulatedFees[refereeUser.network_id]) { + // refererKey includes user kit id, user network id, and user email separated by colons + const refererKey = `${referer.id}:${referer.network_id}:${referer.email}`; + if (!filteredFees[refererKey]) { + filteredFees[refererKey] = {}; + } + + for (let coin in accumulatedFees[refereeUser.network_id]) { + if (!isNumber(filteredFees[refererKey][coin])) { + filteredFees[refererKey][coin] = 0; + } + + filteredFees[refererKey][coin] = addAmounts( + filteredFees[refererKey][coin], + accumulatedFees[refereeUser.network_id][coin] + ); + + + const refIndex = referralHistory.findIndex(data => data.referee === refereeUser.id && data.referer === referer.id && data.coin === coin); + if (refIndex >= 0) { + referralHistory[refIndex].accumulated_fees = filteredFees[refererKey][coin]; + } else { + referralHistory.push({ + referer: referer.id, + referee: refereeUser.id, + last_settled: lastSettledTrade, + code: affiliation.code, + earning_rate: affiliation.earning_rate, + coin, + accumulated_fees: filteredFees[refererKey][coin] + }); + } + } + } + } + + const nativeCurrency = getKitConfig()?.referral_history_config?.currency; + + if (!nativeCurrency) { + throw new Error('currency in referral config not defined'); + } + + const exchangeCoins = referralHistory.map(record => record.coin) || []; + const conversions = await getNodeLib().getOraclePrices(exchangeCoins, { + quote: nativeCurrency, + amount: 1 + }); + + for (let record of referralHistory) { + record.accumulated_fees = applyEarningRate(record.accumulated_fees, record.earning_rate); + + if (conversions[record.coin] === -1) continue; + record.accumulated_fees = new BigNumber(record.accumulated_fees).multipliedBy(conversions[record.coin]).toNumber(); + record.status = false; + } + + return referralHistoryModel.bulkCreate(referralHistory); + }) + .catch(err => err); +}; + +const settleFees = async (user_id) => { + const { active, distributor_id, minimum_amount } = getKitConfig()?.referral_history_config || {}; + if (!active) { + throw new Error(REFERRAL_HISTORY_NOT_ACTIVE); + } + + const exchangeInfo = getKitConfig().info; + + if (!REFERRAL_HISTORY_SUPPORTED_PLANS.includes(exchangeInfo.plan)) { + throw new Error(REFERRAL_UNSUPPORTED_EXCHANGE_PLAN); + } + + const distributor = await getUserByKitId(distributor_id, true, true); + + const nativeCurrency = getKitConfig()?.referral_history_config?.currency; + + if (!nativeCurrency) { + throw new Error('currency in referral config not defined'); + } + + const { transferAssetByKitIds} = require('./wallet'); + const referralHistoryModel = getModel('ReferralHistory'); + + const unrealizedRecords = await referralHistoryModel.findAll({ + where: { referer: user_id, status: false }, + }); + + let totalValue = 0; + for (let record of unrealizedRecords) { + totalValue = new BigNumber(record.accumulated_fees).plus(totalValue).toNumber(); + } + + if (totalValue < minimum_amount) { + throw new Error('Total unrealized earned fees are too small to be converted to realized earnings'); + } + + if (distributor.balance[`${nativeCurrency}_available`] < totalValue) { + // send email to admin for insufficient balance + sendEmail( + MAILTYPE.ALERT, + null, + { + type: 'Insufficient balance for fee settlement!', + data: `

Distributor with ID ${distributor_id} does not have enough balance to proceed with the settlement, Required amount: ${totalValue} ${nativeCurrency.toUpperCase()}, Available Amount: ${distributor.balance[`${nativeCurrency}_available`]} ${nativeCurrency.toUpperCase()}

` + }, + {} + ); + + throw new Error('Settlement is not available at the moment, please retry later'); + } + + try { + const settledIds = unrealizedRecords.map(record => record.id); + await referralHistoryModel.update({ status: true }, { where : { id : settledIds }}); + + await transferAssetByKitIds( + distributor_id, + user_id, + nativeCurrency, + totalValue, + 'Referral Settlement', + false + ); + + } catch (error) { + // send mail to admin + sendEmail( + MAILTYPE.ALERT, + null, + { + type: 'Error during fee settlement!', + data: `

Error occured during a fee settlement operation for user id: ${user_id}, error message: ${error.message}

` + }, + {} + ); + + // obfuscate the message for the end user + throw new Error('Something went wrong'); + } +}; + +const fetchUserReferrals = async (opts = { + user_id: null, + limit: null, + page: null, + order_by: null, + order: null, + start_date: null, + end_date: null, + format: null +}) => { + const referralHistoryModel = getModel('ReferralHistory'); + const timeframe = timeframeQuery(opts.start_date, opts.end_date); + + const dateTruc = fn('date_trunc', 'day', col('last_settled')); + let query = { + where: { + referer: opts.user_id + }, + attributes: [ + [fn('sum', col('accumulated_fees')), 'accumulated_fees'] + ], + group: [] + }; + + if (!opts.format) { query.where.created_at = timeframe; query = {...query };} + + if (opts.order_by === 'referee') { + query.attributes.push('referee'); + query.group.push('referee'); + + return referralHistoryModel.findAll(query) + .then(async (referrals) => { + return { count: referrals.length , data: referrals }; + }); + } else { + query.attributes.push([dateTruc, 'date']); + query.group.push('date'); + + let result = {}; + let referrals = await referralHistoryModel.findAll(query); + result = { count: referrals.length , data: referrals }; + + query = { + where: { + referer: opts.user_id + }, + attributes: [ + [fn('sum', col('accumulated_fees')), 'accumulated_fees'] + ], + group: [] + }; + + referrals = await referralHistoryModel.findAll(query); + result.total = referrals?.[0]?.accumulated_fees; + return result; + } +}; + const getUserBalanceHistory = (opts = { user_id: null, limit: null, @@ -2640,6 +3203,9 @@ const fetchUserProfitLossInfo = async (user_id, opts = { period: 7 }) => { const finalBalances = filteredBalanceHistory[filteredBalanceHistory.length - 1].balance; results[interval] = {}; + let totalCumulativePNL = 0; + let totalInitialValue = 0; + let totalFinalValue = 0; Object.keys(finalBalances).forEach(async (asset) => { if (initialBalances?.[asset] && initialBalances?.[asset]?.native_currency_value) { const cumulativePNL = @@ -2659,39 +3225,17 @@ const fetchUserProfitLossInfo = async (user_id, opts = { period: 7 }) => { cumulativePNL, cumulativePNLPercentage, }; + + totalCumulativePNL += cumulativePNL; + totalInitialValue += day1Assets + inflow; + totalFinalValue += finalBalances[asset].native_currency_value; } }); - } - - const weightedAverage = (prices, weights) => { - const [sum, weightSum] = weights.reduce( - (acc, w, i) => { - acc[0] = acc[0] + prices[i] * w; - acc[1] = acc[1] + w; - return acc; - }, - [0, 0] - ); - return sum / weightSum; - }; - - for (const interval of ['7d', '1m', '3m']) { - if (results[interval]) { - let total = 0; - let percentageValues = []; - let prices = []; - const assets = Object.keys(results[interval]); + results[interval].total = totalCumulativePNL; - assets?.forEach(asset => { - total += results[interval][asset].cumulativePNL; - if (conversions[asset]) { - prices.push(conversions[asset]); - percentageValues.push(results[interval][asset].cumulativePNLPercentage); - } - }); - results[interval].total = total; - const weightedPercentage = weightedAverage(percentageValues, prices); - results[interval].totalPercentage = weightedPercentage ? weightedPercentage.toFixed(2) : null; + if (totalInitialValue !== 0) { + const totalCumulativePNLPercentage = (totalCumulativePNL / totalInitialValue) * 100; + results[interval].totalPercentage = totalCumulativePNLPercentage ? totalCumulativePNLPercentage.toFixed(2) : null; } } @@ -2700,6 +3244,7 @@ const fetchUserProfitLossInfo = async (user_id, opts = { period: 7 }) => { return results; }; + module.exports = { loginUser, getUserTier, @@ -2762,5 +3307,15 @@ module.exports = { changeKitUserEmail, storeVerificationCode, signUpUser, - verifyUser + verifyUser, + getAllAffiliations, + applyEarningRate, + addAmounts, + settleFees, + getUnrealizedReferral, + getRealizedReferral, + fetchUserReferrals, + createUnrealizedReferralFees, + getUserReferralCodes, + createUserReferralCode }; diff --git a/server/utils/hollaex-tools-lib/tools/wallet.js b/server/utils/hollaex-tools-lib/tools/wallet.js index 1b5cc9783c..27acf5c4fb 100644 --- a/server/utils/hollaex-tools-lib/tools/wallet.js +++ b/server/utils/hollaex-tools-lib/tools/wallet.js @@ -37,6 +37,9 @@ const { isEmail } = require('validator'); const BigNumber = require('bignumber.js'); const isValidAddress = (currency, address, network) => { + if (address.indexOf('://') > -1) { + return false; + } if (network === 'eth' || network === 'ethereum') { return WAValidator.validate(address, 'eth'); } else if (network === 'stellar' || network === 'xlm') { @@ -346,7 +349,7 @@ const calculateWithdrawalMax = async (user_id, currency, selectedNetwork) => { amount = new BigNumber(balance[`${currency}_available`]).minus(new BigNumber(fee)).toNumber(); - if (coinMarkup?.fee_markup) { + if (coinMarkup?.fee_markup && selectedNetwork !== 'email') { amount = new BigNumber(amount).minus(new BigNumber(coinMarkup.fee_markup)).toNumber(); } } @@ -411,7 +414,7 @@ const validateWithdrawal = async (user, address, amount, currency, network = nul const balance = await getNodeLib().getUserBalance(user.network_id); - if (coinMarkup?.fee_markup && network !== 'fiat') { + if (coinMarkup?.fee_markup && network !== 'fiat' && network !== 'email') { fee = math.number(math.add(math.bignumber(fee), math.bignumber(coinMarkup.fee_markup))); } diff --git a/server/ws/chat/index.js b/server/ws/chat/index.js index 2e62de30c8..a820bd5618 100644 --- a/server/ws/chat/index.js +++ b/server/ws/chat/index.js @@ -3,7 +3,8 @@ const { debounce, each } = require('lodash'); const { CHAT_MESSAGE_CHANNEL, CHAT_MAX_MESSAGES, - WEBSOCKET_CHANNEL + WEBSOCKET_CHANNEL, + P2P_CHAT_MESSAGE_CHANNEL } = require('../../constants'); const { storeData, restoreData } = require('./utils'); const { isUserBanned } = require('./ban'); @@ -17,6 +18,8 @@ let MESSAGES = []; // redis subscriber, get message and updates MESSAGES array subscriber.subscribe(CHAT_MESSAGE_CHANNEL); +subscriber.subscribe(P2P_CHAT_MESSAGE_CHANNEL); + subscriber.on('message', (channel, data) => { if (channel === CHAT_MESSAGE_CHANNEL) { data = JSON.parse(data); @@ -25,6 +28,13 @@ subscriber.on('message', (channel, data) => { } else if (data.type === 'deleteMessage') { MESSAGES.splice(data.data, 1); } + } else if(P2P_CHAT_MESSAGE_CHANNEL) { + data = JSON.parse(data); + if (data.type === 'message') { + publishP2PChatMessage('addMessage', data.data); + } else if(data.type === 'status') { + publishP2PChatMessage('getStatus', data.data); + } } }); @@ -58,6 +68,29 @@ const addMessage = (username, verification_level, userId, message) => { } }; +const addP2PMessage = (user_id, p2pData) => { + const created_at = moment(); + const data = { + id: p2pData.id, + user_id, + ...p2pData, + created_at + }; + publisher.publish(P2P_CHAT_MESSAGE_CHANNEL, JSON.stringify({ type: 'message', data })); +}; + +const getP2PStatus = (user_id, p2pData) => { + const created_at = moment(); + const data = { + id: p2pData.id, + user_id, + status: p2pData.status, + created_at + }; + publisher.publish(P2P_CHAT_MESSAGE_CHANNEL, JSON.stringify({ type: 'status', data })); + +}; + const deleteMessage = (idToDelete) => { const indexOfMessage = MESSAGES.findIndex(({ id }) => id === idToDelete); if (indexOfMessage > -1) { @@ -79,6 +112,20 @@ const publishChatMessage = (event, data) => { }); }; +const publishP2PChatMessage = (event, data) => { + const topic = `p2pChat${data.id}` + + each(getChannels()[WEBSOCKET_CHANNEL('p2pChat', data.id)], (ws) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + topic: topic, + action: event, + data + })); + } + }); +}; + const maintenanceMessageList = debounce(() => { MESSAGES = getMessages(); storeData(MESSAGES_KEY, MESSAGES); @@ -93,5 +140,7 @@ module.exports = { addMessage, deleteMessage, publishChatMessage, - sendInitialMessages + sendInitialMessages, + addP2PMessage, + getP2PStatus }; diff --git a/server/ws/index.js b/server/ws/index.js index 235c4b41b9..1f26458805 100644 --- a/server/ws/index.js +++ b/server/ws/index.js @@ -10,7 +10,7 @@ const { WS_UNSUPPORTED_OPERATION, WS_USER_AUTHENTICATED } = require('../messages'); -const { initializeTopic, terminateTopic, authorizeUser, terminateClosedChannels, handleChatData } = require('./sub'); +const { initializeTopic, terminateTopic, authorizeUser, terminateClosedChannels, handleChatData, handleP2pData } = require('./sub'); const { connect, hubConnected } = require('./hub'); const { setWsHeartbeat } = require('ws-heartbeat/server'); const WebSocket = require('ws'); @@ -69,6 +69,13 @@ wss.on('connection', (ws, req) => { const { action, data } = arg; handleChatData(action, ws, data); }); + } + else if (op === 'p2pChat') { + loggerWebsocket.info(ws.id, 'ws/index/message', message); + args.forEach((arg) => { + const { action, data } = arg; + handleP2pData(action, ws, data); + }); } else { throw new Error(WS_UNSUPPORTED_OPERATION); } diff --git a/server/ws/sub.js b/server/ws/sub.js index 8f6dcdd4fa..a13867ffc9 100644 --- a/server/ws/sub.js +++ b/server/ws/sub.js @@ -16,7 +16,7 @@ const { NOT_AUTHORIZED } = require('../messages'); const { subscriber } = require('../db/pubsub'); -const { sendInitialMessages, addMessage, deleteMessage } = require('./chat'); +const { sendInitialMessages, addMessage, deleteMessage, addP2PMessage, getP2PStatus } = require('./chat'); const { getUsername, changeUsername } = require('./chat/username'); const { sendBannedUsers, banUser, unbanUser } = require('./chat/ban'); const { sendNetworkWsMessage } = require('./hub'); @@ -101,6 +101,10 @@ const initializeTopic = (topic, ws, symbol) => { addSubscriber(WEBSOCKET_CHANNEL(topic), ws); sendInitialMessages(ws); break; + + case 'p2pChat': + addSubscriber(WEBSOCKET_CHANNEL(topic, symbol), ws); + break; case 'admin': // this channel can only be subscribed by the exchange admin if (!ws.auth.sub) { @@ -170,6 +174,10 @@ const terminateTopic = (topic, ws, symbol) => { removeSubscriber(WEBSOCKET_CHANNEL(topic), ws); ws.send(JSON.stringify({ message: `Unsubscribed from channel ${topic}:${ws.auth.sub.id}` })); break; + case 'p2pChat': + removeSubscriber(WEBSOCKET_CHANNEL(topic, symbol), ws); + ws.send(JSON.stringify({ message: `Unsubscribed from channel ${topic}:${ws.auth.sub.id}` })); + break; case 'admin': if (!ws.auth.sub) { throw new Error(WS_AUTHENTICATION_REQUIRED); @@ -323,6 +331,30 @@ const handleChatData = (action, ws, data) => { }); }; +const handleP2pData = (action, ws, data) => { + if (!ws.auth.sub) { + throw new Error('Not authorized'); + } else if (action === 'deleteMessage' || action === 'getBannedUsers' || action === 'banUser' || action === 'unbanUser') { + if ( + ws.auth.scopes.indexOf(ROLES.ADMIN) === -1 && + ws.auth.scopes.indexOf(ROLES.SUPERVISOR) === -1 && + ws.auth.scopes.indexOf(ROLES.SUPPORT) === -1 + ) { + throw new Error('Not authorized'); + } + } + switch (action) { + case 'addMessage': + addP2PMessage(ws.auth.sub.id, data); + break; + case 'getStatus': + getP2PStatus(ws.auth.sub.id, data) + break; + default: + throw new Error('Invalid action'); + } +}; + const handleDepositWithdrawalData = (data) => { switch (data.topic) { case 'deposit': @@ -352,5 +384,6 @@ module.exports = { terminateTopic, authorizeUser, terminateClosedChannels, - handleChatData + handleChatData, + handleP2pData }; diff --git a/version b/version index 55f001e46c..ed0edc885b 100644 --- a/version +++ b/version @@ -1 +1 @@ -2.10.4 \ No newline at end of file +2.11.0 \ No newline at end of file diff --git a/web/diff.json b/web/diff.json deleted file mode 100644 index 08d133f9d2..0000000000 --- a/web/diff.json +++ /dev/null @@ -1,508 +0,0 @@ -{ - "DEFAULT_TIMESTAMP_FORMAT": "DD, MMMM, YYYY", - "NO_ACTIVE_ORDERS": ".هیچ سفارشی پیدا نشد، سفارش خود را در خرید و فروش آسان یا حرفه ثبت نمایید", - "NO_ACTIVE_TRADES": ".به نظر میرسد که سفارشی هنوز موجود نیست", - "NO_ACTIVE_DEPOSITS": ".به نظر میرسد که هنوز واریزی انجام نشده است", - "NO_ACTIVE_WITHDRAWALS": ".به نظر میرسد که هنوز برداشتی انجام نشده است", - "HELP_RESOURCE_GUIDE": { - "CONTACT_US": "تماس با ما", - "TEXT": "به آسانی به {0} برای دریافت اطلاعات بیشتر یا حل مشکلات خود ایمیل ارسال نمایید" - }, - "CONVERT": "تبدیل", - "HOME": { - "MAIN_TITLE": "صرافی معاملات رمزارز", - "MAIN_TEXT": ".به آسانی دارایی های کریپتو خود را خرید و فروش نمایید. به سادگی با ایمیل خود حساب جدید بسازید و دارایی های اصلی کریپتو خود در هر زمان معامله نمایید", - "TRADE_CRYPTO": "شروع معاملات", - "VIEW_EXCHANGE": "مشاهده صرافی" - }, - "FOOTER": { - "TERMS_OF_SERVICE": "شرایط و مقررات سرویس دهی", - "PRIVACY_POLICY": " سياست حفظ حريم خصوصي اشخاص" - }, - "ACCOUNTS": { - "TAB_APPS": "برنامه ها", - "TAB_HISTORY": "تاریخچه", - "TAB_FIAT": "Fiat controls" - }, - "DEPOSIT": { "CRYPTO_LABELS": { "MEMO": "Your {0} memo" } }, - "VALIDATIONS": { - "MIN_VALUE_NE": ".قیمت باید بیشتر از {0} باشد", - "MAX_VALUE_NE": ".قیمت باید کمتر از {0} باشد " - }, - "OTP_FORM": { - "OTP_FORM_INFO": ".برای ادامه عملیات کد 6 رقمی خود را وارد نمایید", - "OTP_FORM_SUBNOTE_LINE_1": ".شناخته میشود two-factor authenticator (2FA) یا OTP codeکد شما به عنوان ", - "OTP_FORM_SUBNOTE_LINE_2": ".اگر کد خود را گم کرده اید، لطفا با پشتیبانی تماس بگیرید" - }, - "EMAIL_CODE_FORM": { - "TITLE": "ورودی کدهای امنیتی", - "LABEL": "ورودی کد(لطفا ایمیل هود را چک نمایید)", - "PLACEHOLDER": "کد ارسال شده در ایمیل را وارد نمایید", - "FORM_TITLE": " .کد ویژه ای که به ایمیل شما ارسال شده برای اتمام عملیات الزامی میباشد. لطفا کد ارسالی به ایمیل تان و کد اتی پی را وارد نمایید", - "ERROR_INVALID": ".کدی صحیح نمی باشد. لطفا بار دیگر وارد نمایید", - "OTP_LABEL": "2FA کد(OTP)", - "OTP_PLACEHOLDER": ".را وارد نمایید two-factor authentication کد 6 رقمی" - }, - "QUICK_TRADE_COMPONENT": { - "INFO": "بهترین و سریع ترین راه برای معامله کریپتوی شما", - "CHANGE_TEXT": "تبدیل", - "HIGH_24H": "بالا ترین در 24 ساعت", - "LOW_24H": "کمترین در 24 ساعت", - "BEST_BID": "BID بهترین", - "FOOTER_TEXT": ".قیمت خرید و فروش آسان از نرخ سفارش بردار گرفته شده ", - "FOOTER_TEXT_1": "برگفته شده از", - "GO_TO_TEXT": "برو به", - "SOURCE_TEXT": "OTC پیشنهاد کارگزار" - }, - "WALLET": { "LOADING_ASSETS": "...بار گذاری دارایی ها" }, - "VERIFICATION_EMAIL_REQUEST": { - "SUBTITLE": ".بار دیگر درخواست ایمیل تایید را در ذیل ثبت نمایید", - "SUPPORT": "تماس با پشتیبانی" - }, - "USER_VERIFICATION": { - "EMAIL_VERIFICATION": "ارسال ایمیل تایید", - "VERIFICATION_SENT": " تاییدیه ارسال شد", - "VERIFICATION_SENT_INFO": ".ایمیل خود را چک کرده و بروی لینک مربوطه کلیک کنید تا ایمیل تایید شود.", - "ID_DOCUMENTS_FORM": { - "INFORMATION": { - "ID_SECTION": { - "LIST_ITEM_0": " .اندازه کل حجم مدارک نباید بیشتر از{0} مگابایت باشد", - "VIOLATION_ERROR": ".اندازه کل حجم مدارک بارگذاری شده بیشتر از{0} مگابایت است. لطفا برای ادامه عملیات فایل های با حجم کمتر بار گذاری نمایید" - }, - "POR": { - "WARNING": " .ما نمی توانیم آدرس مرقوم شده در مدرک شناسایی ارسالی شما را به عنوان مدرک معتبر سکونت درنظر بگیریم" - } - } - }, - "TITLE_PAYMENT": "اضافه کردن روشهای پرداخت", - "PAYMENT_VERIFICATION": "تایید پرداخت", - "START_PAYMENT_VERIFICATION": "َشروع تایید", - "PAYMENT_VERIFICATION_TEXT_1": ".اطلاعات حساب پرداخت خود را در ذیل وارد نمایید", - "PAYMENT_VERIFICATION_TEXT_2": ".بعد از تایید حساب شما، شما قادر خواهید بود که روشهای مختلفی برای واریز یا دریافت ارز های مختلف داشته باشید", - "ADD_ANOTHER_PAYMENT_METHOD": "اضافه کردن روش پرداخت دیگر", - "PAYMENT_VERIFICATION_HELP_TEXT": ".برای تایید در این بخش، تکمیل بخش{0} ضروری است" - }, - "USER_SETTINGS": { - "AUDIO_CUE_FORM": { - "ALL_AUDIO": "تمام نشانه های صوتی", - "ORDERS_PLACED_AUDIO": "وقتی که سفارشی ثبت مبشود", - "ORDERS_CANCELED_AUDIO": "وقتی که سفارشی لغو میشود", - "CLICK_AMOUNTS_AUDIO": " وقتی که روی قیمت و مقدار در سفارشات", - "GET_QUICK_TRADE_AUDIO": ".وقتی که قیمت را از خرید و فروش آسان میگیرید", - "SUCCESS_QUICK_TRADE_AUDIO": ".وقتی که معامله با موفقیت در خرید و فروش آسان انجام شود", - "QUICK_TRADE_TIMEOUT_AUDIO": " .وقتی مهلت در خرید و فروش آسان تمام میشود" - } - }, - "USER_APPS": { - "TITLE": "برنامه های صرافی شما", - "SUBTITLE": "اطلاعات حساب صرافی شما و بسیاری از قابلیت های آن در ذیل", - "ALL_APPS": { - "TAB_TITLE": "تمام برنامه ها", - "TITLE": "برنامه های صرافی", - "SUBTITLE": ".با کلیک کردن روی دکمه زیر به سادگی قابلیت های حساب رافی خود را افزایش دهید", - "SEARCH_PLACEHOLDER": "...جستجوی برنامه ها ", - "ADD": { - "SUCCESSFUL": "!شما موفق شدید برنامه جدیدی را اضافه نمایید", - "FAILED": "اشتباهی رخ داده است" - } - }, - "MY_APPS": { - "TAB_TITLE": "برنامه های من", - "TITLE": "برنامه های صرافی من", - "SUBTITLE": ".در قسمت زیر میتوانیید برنامه های فعال صرافی را مشاهده کنید. شمامی توانیدبا کلیک کردن روی برنامه ها اطلاعات آنهارا مشاهده کنید و در صورت تمایل اقدام به حذف و اضافه آنها نمایید. برنامه برای آن تهییه شده تا شما قابلیت های دلخواه خود را در صرافی خود تجربه کنید" - }, - "TABLE": { - "APP_NAME": "نام برنامه", - "DESCRIPTION": "توضیحات", - "ACTION": "عملیات", - "CONFIGURE": "پیکربندی", - "VIEW_APP": "مشاهده برنامه", - "ADD": "اضافه", - "NOT_FOUND": "...نمی توانم این برنامه را پیدا کنم ", - "RETRY": "لغت دیگری را برای جستحو انتخاب کنید" - }, - "APP_DETAILS": { - "BACK_PLACEHOLDER": "{0} {1}", - "BACK_TO_APPS": "برو به برنامه های من", - "BACK": "بازگشت" - }, - "CONFIGURE": { - "TITLE": "پیکربندی برنامه", - "SUBTITLE": ":برنامه خود را در قسمت زیر پیکربندی کنید", - "REMOVE": "حذف برنامه", - "TEXT": ".اگر در برنامه خود با مشکلی مواجه شدید لظفا با کلیک کردن روی گزینه کمک با ما تماس بگیرید", - "BACK": "بازگشت", - "HELP": "کمک" - }, - "REMOVE": { - "TITLE": "حذف برنامه", - "SUBTITLE": ".این برنامه از لیست برنامه های من حذف خواهد شد", - "TEXT": "آیا مطمن هستید که میخواهید این برنامه را حذف کنید؟", - "BACK": "بازگشت", - "CONFIRM": "تایید" - } - }, - "ACCOUNT_SECURITY": { "LOGIN": { "TIME": "زمان/ تاریخ" } }, - "STAKE": { - "NETWORK_WARNING": "شبکه شما ناسازگار است. لطفا شبکه خودرا به{0} تغییر دهید", - "EARN": "درآمدزایی", - "TITLE": "گرو گذاری", - "MODAL_TITLE": "گرو گذاری و در آمد {0}", - "REVIEW_MODAL_TITLE": "بررسی و تایید گرو", - "AVAILABLE_TOKEN": " در{0} ({1})موجود برای گرو گذاری: {2}", - "DEFI_TITLE": "کردن دارایی های گروگذاری شده ِDeFi", - "DEFI_TEXT": ".در روش گروگذاری به کمک دی فای میتوانید با برقراری اتصال به کیف پول مجازی خود به طور مستقیم گرو گذرای و کسب در آمد کنید", - "CURRENT_ETH_BLOCK": "{0}:ETH بلاک کنونی", - "ON_EXCHANGE_XHT": " {1} {2} در صرافی {0} مقدار موجودی برابراست با", - "LOGIN_HERE": "از اینجا وارد شوید", - "MOVE_XHT": "{0} انتقال", - "ESTIMATED_STAKED": "مقدرا تخمینی گروگذاری", - "ESTIMATED_EARNINGS": "مقدار تخمینی درآمدها", - "CONNECT_WALLET": "اتصال به کیف پول", - "NEXT": "بعدی", - "REVIEW": "بازبینی", - "ESTIMATED": "تخمینی", - "BLOCK": "بلاک", - "CANCEL": "ابطال", - "PROCEED": "قبول کردم", - "GO_TO_WALLET": "برو به کیف پول", - "AMOUNT_LABEL": "مقدار گرو گذاری", - "PERIOD_SUBTITLE": "اگر زمان بیشتری گروگذاری کنید سودی بیشتری هم خواهید کرد. برای تعیین زمان گرو گذاری بخش زیر را پر نمایید", - "STAKE_AND_EARN_DETAILS": " {1}گروگذاری برای~{0} درآمد برای", - "PREDICTED_EARNINGS": "در آمد پیشبینی شده", - "VARIABLE_TEXT": "*{0} در باره اینکه چگونه نرخ متغییر ها کار می کنند", - "READ_MORE": "بیشتر بخوانید", - "CURRENT_BLOCK": "{0}:بلاک کنونی", - "END_BLOCK": "{0}:آخرین بلاک", - "DURATION": "مدت زمان", - "END_ON_BLOCK": "{0}: بروی آخرین بلاک", - "SLASHING_TITLE": "(early unstake)اسلاشینگ", - "SLASHING_TEXT_1": "{0}% of your stakes principle", - "SLASHING_TEXT_2": "All earnings forfeited", - "REVIEW_NOTE": ".مدت زمان به زمان بندی بلوک های اتریوم بستگی دارد. لطفاً قبل از گروگذاری، جزئیات بالا راپیش از گرو گذاری بررسی و تأیید کنید، زیرا برداشت گرو زودهنگام باعث می‌شود درصدی از اصل گروی شما کسر شود و درآمد شم زیان ببیند.", - "WAITING_TITLE": "انتظار برای تایید", - "WAITING_TEXT": "این تراکنش را در کیف پول خود تایید کنید", - "PENDING_TEXT": "...تراکنش در حال انتظار است", - "CHECKING_ALLOWANCE": "...در حال بررسی {0} برای پذیرش", - "WAITING_STAKE": "مقدار گرو را تایید نمایید", - "WAITING_WITHDRAW": "اجازه خرج کردن", - "WAITING_UNSTAKE": "برداشت گرو", - "WAITING_STAKE_ING": "گروگذاری در حال انتطار است", - "WAITING_WITHDRAW_ING": "اجازه خرج کردن در حال انجام است", - "WAITING_UNSTAKE_ING": "گرو برداری", - "SUCCESSFUL_STAKE_TITLE": "{0}شما با موفقیت گروگذاری کرده اید", - "SUCCESSFUL_STAKE_AMOUNT": "مقدار گروگذاری شده", - "SUCCESSFUL_STAKE_DURATION_DEF": " ({1}){0}پایان بلاک ", - "SUCCESSFUL_STAKE_DESTINATION": "مقصد", - "SUCCESSFUL_UNSTAKE_ADDRESS": "آدرس من", - "ERROR_TITLE": " خطا: {0}مردود است", - "ERROR_SUBTITLE": ".این یک اشتباه باشد، می توانید به عقب برگردید و دوباره امتحان کنید", - "SUCCESSFUL_UNSTAKE_TITLE": "{0}شما با موفقیت گروبدراری کرده اید", - "SUCCESSFUL_UNSTAKE_AMOUNT": "مجموع که میتوانید دریافت کنید", - "EARNINGS": "درآمدها", - "ORIGINAL_AMOUNT": "مقدار اولیه گرو گذاری شده", - "CONNECT_A_WALLET": "اتصال به کیف پول", - "CONNECT_WALLET_TABLE": "برای {0}مشاهده سابقه اتفاقات گرو", - "ZERO_STAKES": "گرو های صفر", - "VIEW_ON": "بر روی {0}مشاهده کنید", - "BLOCKCHAIN": "بلاکچین", - "VIEW_POT": "POT مشاهده توزیع", - "COMPLETED": "وقتش شده", - "COMPLETED_TOOLTIP": "گروی شما به رشد کافی رسیده Continue staking to earn more rewards or unstake to claim rewards.", - "CONNECT_ERROR": "لطفا کیف پول خود را چک کنید", - "INSTALL_METAMASK": " https://metamask.io/download.html :شما باید متا مکس را در مرورگر خود نصب کنید ", - "INSTALL_METAMASK_TITLE": "متامکس وجود ندارد", - "REWARDS": { - "0": { - "CARD": "پاداش درآمد ها(بدون بونس)", - "TEXT": "پادش های عادی " - }, - "1": { - "CARD": " پاداش درآمد به علاوه بونس", - "TEXT": "پاداش بونس بروی درامد" - }, - "2": { - "CARD": "بالا ترین بونس ، بالاترین پاداش", - "TEXT": "بالاترین پاداش بونس بر پاداش درآمد" - }, - "3": { "CARD": "", "TEXT": "" }, - "4": { "CARD": "", "TEXT": "" } - } - }, - "UNSTAKE": { - "EARLY_TITLE": "گرو برداری زودهنگام", - "EARLY_WARNING_TITLE": "به نطر میرشد شما قصد گرو برداری پیش از موعد را دارید", - "EARLY_WARNING_TEXT_1": " .برداشت گرو زودهنگام باعث می‌شود درصدی از اصل اولیه گرو شما کسر شود و درآمد شم زیان ببیند", - "EARLY_WARNING_TEXT_2": "آیا مطمئن هستید که می خواهید ادامه دهید؟", - "BACK": "بازگشت", - "REVIEW": "حذف گرو", - "DURATION": "تخمین دوره رشد و سودهی", - "PROCEED": "انجام شد", - "EARNINGS_FORFEITED": "درآمد ها ضبط شده", - "EST_PENDING": "{0}:تخمین تعلیق", - "AMOUNT_SLASHED": " *مقدار اسلش شده", - "AMOUNT_TO_RECEIVE": "مقدار که باید دریافت شود", - "SLASH_FOOTNOTE": "*تمام مقادیر اسلش شده بین گروگذاران دیگر توزیع می شود. لطفاً مبلغ اسلش شده را از اصل اولیه، درآمدهای ضبط شده و مدت زمان باقیمانده در نظر بگیرید و تصمیم بگیرید که آیا ارزش از دست رفته در گروبرداری زود هنگام ارزش هزینه را دارد یا خیر.", - "AMOUNT_NOTE": "مبالغ به آدرس کیف پول شما توزیع می شود", - "TOTAL_EARNT": "کل درآمد حاصل شده", - "PENDING_EARNINGS": "*درآمدهای پرداخت نشده", - "PENDING_EARNINGS_FOOTNOTE": "*درآمدهای پرداخت نشده مبالغی هستند که تسویه نشده اند و به تراکنش بلاک چین نیاز دارند تا به کل مبلغ دریافتی شما اضافه شود" - }, - "STAKE_TABLE": { - "AVAILABLE": "در دسترس برای گروگذاری", - "TOTAL": "مجموع گروگذاری شده", - "REWARD_RATE": "نرخ پاداش", - "EARNINGS": "درآمدها", - "STAKE": "گرو", - "VARIABLE": "متغییر" - }, - "STAKE_LIST": { - "AMOUNT": "مقادیر گروگذاری شده", - "DURATION": "تخمین دوره رشد و سوددهی", - "START": "گروگذاری آغاز شده است", - "EARNINGS": "درآمد", - "STAKE": "گرو" - }, - "STAKE_DETAILS": { - "BACK_SUBTITLE": " به صفحه گروگذاری{0}", - "GO_BACK": "بازگشت", - "CONTRACT_SUBTITLE": "{0}:توکن قرارداد", - "VIEW_MORE": "مشاهده بیشتر", - "TOKEN": " توکن {0}", - "TABS": { - "PUBLIC_INFO": "اطلاعات عمومی", - "DISTRIBUTIONS": "توزیع ها", - "MY_STAKING": "گروگذاری من" - }, - "PUBLIC_INFO": { - "TITLE": "اطلاعات گروگذاری", - "SUBTITLE": "Below is a staking tokenomics for {0} ({1}).", - "TOTAL_DISTRIBUTED_REWARDS": " ({0})مجموع پادشهای توزیع شده ", - "POT_BALANCE": "POT مقدار", - "UNCLAIMED_REWARDS": "پادشهای ", - "TOTAL_STAKED": "مجموع گرو گذاری شده", - "MY_STAKE": "({0}%)گروی من", - "MY_STAKE_PERCENTLESS": "گروی من", - "OTHER_STAKE": "({0}%) گروی دیگر", - "EVENTS_TITLE": "پادش های توزیع شده اخیر" - }, - "DISTRIBUTIONS": { - "TITLE": "پاداش های{0} توزیع شده", - "SUBTITLE": ".{0} در قسمت ذیل لیست سابقه توزیع های انجام شده به گزوگذاران قابل نمایش است", - "TIME": "زمان توزیع", - "TRANSACTION_ID": "تراکنش ID ", - "AMOUNT": "مقدار توزیع شده" - }, - "MY_STAKING": { - "SUBTITLE": ".در قسمت ذیل اطلاعات و سابقه رویدادهای مربوط به گروگذاری {0} شما نمایش داده می شود.", - "EVENTS_TITLE": "سابقه رویداد های مربوط به گرو", - "TIME": "زمان", - "EVENT": "رویداد", - "TRANSACTION_ID": "تراکنش ID", - "AMOUNT": "مقدار" - } - }, - "MOVE_XHT": { - "TITLE": "XHT انتقال", - "TEXT_1": ".برای گرو گذاری ایکس اچ دی خود، ابتدا باید آن رابه کیف پول خود منتقل کنید", - "TEXT_2": "آدرس کیف پول متصل فعلی شما", - "LABEL": "آدرس کیف پول شما", - "TEXT_3": "مهم است که مطمئن شوید آدرس کیف پول فوق امن است. XHT به آدرس کیف پول بالا منتقل می شود." - }, - "MOVE_AMOUNT": { - "TITLE": "مقدار ورودی", - "PROMPT": ".مقداری را که می خواهید جابه جا کنید وارد کنید", - "LABEL": "مقدار جایجایی شده", - "FEE": "{0} {1}: نرخ تراکنش" - }, - "SIDES_VERBS": { - "buy": "خریداری شده", - "sell": "فروخته شده" - }, - "AVERAGE": "قیمت میانگین", - "ORDER_HISTORY": "سابقه سفارشات", - "ESTIMATED_PRICE": "قیست تخمینی", - "WITHDRAWALS_FORM_NETWORK_LABEL": "شبکه", - "DEPOSIT_FORM_NETWORK_WARNING": "اطمینان حاصل کنید که شبکه انتخاب شده با شبکه کیف پول فرستنده سازگار است", - "DEPOSIT_FORM_TITLE_WARNING_DESTINATION_TAG": ".آدرس و تگ را وارد کنید که برای واریز موفقیت آمیز به حساب شما الزامی است", - "WITHDRAW_PAGE_DESTINATION_TAG_MESSAGE": "{0}: تگ مقصد", - "WITHDRAW_PAGE_NETWORK_TYPE_MESSAGE": " {0}نوع آدرس شبکه {1}", - "WITHDRAWALS_FORM_NETWORK_WARNING": "اطمینان حاصل کنید که شبکه انتخاب شده با کیف پول مقصد سازگار است", - "WITHDRAWALS_FORM_FEE_WARNING": "برای برداشت این دارایی لازم است{0} ({1}) ", - "WITHDRAWALS_FORM_DESTINATION_TAG_WARNING": "بررسی کنید که آیا آدرس دریافتی نیاز به تگ دارد یا خیر. همچنین به عنوان ممو، شناسه دیجیتال، برچسب وممو شناخته می شود", - "WITHDRAWALS_FORM_NETWORK_PLACEHOLDER": "شبکه را انتخاب کنید", - "WITHDRAWALS_FORM_MEMO_LABEL": "(اختیاری)ممو ", - "WITHDRAWALS_FORM_FEE_COMMON_LABEL_COIN": "({0})نرخ تراکنش", - "WITHDRAW_PAGE": { - "MESSAGE_FEE_COIN": "{0} نرخ تراکنش مربوط به", - "WITHDRAWALS_FORM_ERROR_TITLE": "اطلاعات انتقال درست نمیباشد", - "WITHDRAWALS_FORM_ERROR": "انتقال وجه شما ناموفق بود. ارسال وجوه به ایمیل مستلزم داشتن حساب کاربری در این صرافی است. بررسی کنید که ایمیل صحیح است و لطفا دوباره امتحان کنید." - }, - "24H_MAX": ":بالاترین در 24 ساعت", - "24H_MIN": ":کمترین در 24ساعت", - "24H_VAL": ":نقدینگی در 24 ساعت", - "WALLET_HIDE_ZERO_BALANCE": "مقدار صفر را نمایش نده", - "WALLET_ESTIMATED_TOTAL_BALANCE": " تخمین مجموع مقدار موجود", - "ORDER_ENTRY_ADVANCED": "پیشرفته", - "QUICK_TRADE_SUCCESS": "!موفقیت", - "QUICK_TRADE_INSUFFICIENT_FUND": "مبلغ ناکافی", - "QUICK_TRADE_INSUFFICIENT_FUND_MESSAGE": ".برای تکمیل این تراکنش،به اندازه کافی در کیف پول خود ندارید", - "QUICK_TRADE_BROKER_NOT_AVAILABLE_MESSAGE": " .پیشنهاد کارگزار اتی سی در حال حاضر در دسترس نمی باشد", - "DEVELOPERS_TOKEN": { - "API_KEY": "API Key", - "SECRET_KEY": "Secret Key", - "ACCESS": "Access", - "BASIC_ACCESS": "Basic access", - "BASIC_ACCESS_PROMPT": "Select what this API key can access.", - "READING_ACCESS": "Reading (wallets balances, etc)", - "TRADING_ACCESS": "Trading", - "IP_ACCESS": "IP access", - "IP_ACCESS_PROMPT": "Configure what IP address will work with this API key.", - "ANY_IP_ADDRESS": "Any IP address", - "ONLY_TRUSTED_IPS": "Only trusted IPs", - "ADD_IP_PH": "Enter IP address. You can add multiple IPs", - "ADVANCED_ACCESS": "Advanced access", - "ADVANCED_ACCESS_PROMPT": "Requires trusted IPs be activated.", - "WITHDRAWAL_ACCESS": "واریز ها", - "SAVE": "ذخیره", - "BEWARE": "!به خاطر داشته باشید، که اجازه برداشت دادن خطرات خاص خود را دارد" - }, - "CHAT": { "SET_USERNAME": "SET USERNAME TO CHAT" }, - "SUMMARY": { - "DEPOSIT_AND_WITHDRAWAL_FEES": "Deposit and withdrawal fees", - "DEPOSITS": "واریزها", - "WITHDRAWALS": "برداشت ها", - "TITLE_OF_ACCOUNT": "{0} Account", - "MARKETS": "بازارها", - "CHANGE_24H": "تغییرات 24 ساعته", - "VOLUME_24H": "نقدینگی 24 ساعته", - "VIEW_MORE_MARKETS": "مشاهده بازارهای بیشتر" - }, - "POST_ONLY": "فقط پست کن", - "CLEAR": "پاک کردن", - "ORDER_TYPE": "نوع", - "ORDER_MODE": "در حالت سفارش", - "TRIGGER_CONDITIONS": "شرایط فعال شدن ماشه", - "TRANSACTION_STATUS": { - "PENDING": "در حال انتطار", - "REJECTED": "مردود", - "COMPLETED": "تکمیل شده" - }, - "DEPOSIT_STATUS": { - "SEARCH_FIELD_LABEL": "شناسه تراکنش خود را جایگذاری کنید", - "CHECK_DEPOSIT_STATUS": "وضعیت واریز را بررسی کنید", - "SEARCH_BLOCKCHAIN_FOR_DEPOSIT": "بلاک چین را برای واریزی خود جستجو کنید", - "STATUS_DESCRIPTION": "شما می توانید با وارد کردن شناسه تراکنش (هش) در قسمت زیر وضعیت سپرده خود را بررسی کنید", - "TRANSACTION_ID": "شناسه تراکنش (هش)", - "SEARCH_SUCCESS": "جستجو تمام شد", - "ADDRESS_FIELD_LABEL": "آدرس خودرا جایگذری کنید", - "CURRENCY_FIELD_LABEL": "ارز را انتخاب کنید" - }, - "CANCEL_ORDERS": { - "HEADING": "ابطال سفارشات", - "SUB_HEADING": "ابطال تمامی سفارشات", - "INFO_1": ".با این کار سفارشات باز شما برای بازار {0} لغو می شود", - "INFO_2": "آیا مطمئن هستید که می خواهید همه سفارشات باز خود را لغو کنید؟" - }, - "LIMITS_BLOCK": { - "HEADER_ROW_DESCRIPTION": " اجازه واریز و برداشت 24 ساعته برای همه دارایی ها ({0})", - "HEADER_ROW_TYPE": "نوع (همه دارایی ها)", - "HEADER_ROW_AMOUNT": "مقدار 24 ساعته ({0})" - }, - "MARKETS_TABLE": { - "TITLE": "Live بازارها", - "MARKETS": "بازارها", - "LAST_PRICE": "آخرین قیمت", - "CHANGE_24H": "انتقال (24 ساعت)", - "VOLUME_24h": "نقدینگی (24 ساعت)", - "CHART_24H": "نمودار (24 ساعت)" - }, - "PAGE_UNDER_CONSTRUCTION": ".این صفحه در حال ساخت است. لطفا در آینده دوباره از این صفحه بازدید کنید", - "UNDEFINED_ERROR_TITLE": "شما با یک خطای ناشناس مواجه شده اید", - "UNDEFINED_ERROR": "وای! یک خطای ناشناخته رخ داده است، این ممکن است یک مشکل اتصال یا موارد دیگر باشد، می‌توانید بعداً دوباره امتحان کنید یا صفحه را رفرش کنید", - "POST_ONLY_TOOLTIP": "هستند پست کنیدlimit order لطفا سفارش های که صرفا", - "REFRESH": "رفرش", - "FEE_REDUCTION": "کاهش کارمزد", - "FEE_REDUCTION_DESCRIPTION": "*.برای حساب شما تخفیف کارمزد اعمال شده است. کاهش کارمزدهای معاملاتی شما بر اساس حساب شما اعمال می شود", - "CHANGE_PASSWORD_FAILED": "تغییر رمز عبور انجام نشد", - "MARKET_OPTIONS": { "CARD": "کارت" }, - "ALL": "همه", - "ASSET_TXT": "دارایی", - "ONE_DAY": " یک روز", - "ONE_WEEK": "یک هفته", - "START_DATE": "تاریخ شروع", - "END_DATE": "تاریخ پایان", - "REGULAR": "منظم", - "STOPS": "استاپ ها", - "VIEW_ALL": "مشاهده همه", - "TRIGGER_PRICE": "قیمت ماشه", - "SPEND_AMOUNT": "مقدار صرف شده", - "ESTIMATE_RECEIVE_AMOUNT": "تخمین مقدار دریافتی", - "TOOLS": { - "CHART": "نمودار", - "PUBLIC_SALES": "فروش عمومی", - "ORDER_ENTRY": "ورودی سفارش", - "RECENT_TRADES": "سفارشات اخیر", - "OPEN_ORDERS": "سفارشات باز", - "DEPTH_CHART": "نموادار عمق", - "COMING_SOON": "به زودی" - }, - "RESET_LAYOUT": "چیدمان اولیه", - "WALLET_BALANCE_LOADING": "...بارگذاری مقدار موجودی", - "CONNECT_VIA_DESKTOP": { - "TITLE": "از طریق دسکتاپ وصل شوید", - "SUBTITLE": "گروگذاری به روش دی فای از طریق موبایل شما در حال حاضر پشتیبانی نمی شود", - "TEXT": ".اتصال کیف پول خود لطفاً از رایانه رومیزی/لپ تاپ استفاده کنید" - }, - "FIAT": { - "UNVERIFIED": { - "TITLE": "اتمام عملیات تایید", - "TEXT": ".ایجاد یک {0} باید تأیید خود را تکمیل کنید که شامل تأیید جزئیات بانک شما می شود. لطفا روی دکمه ادامه درقسمت زیر کلیک کنید.", - "DEPOSIT": "واریز", - "WITHDRAWAL": "برداشت" - }, - "REVIEW_DEPOSIT": { - "TITLE": "جزئیات واریز را بررسی و تأیید کنید", - "SUBTITLE": ".لطفاً جزئیات واریز زیر را بررسی کنید تا مطمئن شوید همه چیز درست است", - "FORMAT": "{0} {1}", - "AMOUNT": "مقدار واریزی", - "FEE": "کارمزد واریز", - "TRANSACTION_ID": "شناسه تراکنش", - "NOTE": ".برای جلوگیری از تاخیر، مطمئن شوید که مبلغ واریز، ممو و شناسه تراکنش شما با جزئیات بالا مطابقت داشته باشد، در صورت بروز مشکل لطفا با پشتیبانی تماس بگیرید", - "BACK": "بازگشت", - "PROCEED": "ادامه" - } - }, - "DEPOSIT_FEE_NOTE": ".توجه داشته باشید، کل مبلغ واریز شده به حساب شما، منهای مبلغ کارمزد واریز خواهد بود", - "AMOUNT_LABEL": "مبلغ واریز شده را وارد کنید (باید با مبلغ واقعی یکسان باشد)", - "TRANSACTION_ID_LABEL": "شناسه تراکنش واریزه را وارد کنید", - "FEE_LABEL": "کارمزد واریز", - "AMOUNT_FORMAT": "{0} {1}", - "PENDING_DEPOSIT_TITLE": "واریز در حال انتظار", - "PENDING_DEPOSIT_TEXT_1": ".واریزی شما اکنون در صف تأیید است و تا زمانی که پاک شود در حالت معلق باقی می ماند", - "PENDING_DEPOSIT_TEXT_2": ".معمولاً واریزها ظرف 24 ساعت تسویه می‌شوند، اما ممکن است تا 48 ساعت طول بکشد، پس از تسویه مبلغ سپرده شما منهای کارمزد، مبلغ واریزی به موجودی شما اضافه می‌شود", - "DEPOSIT_TXID_NOTE": "برای جلوگیری از تاخیر در واریزی، لطفاً هنگام واریز بانکی خود، یادداشت منحصر به فرد خود را که در بالا نشان داده شده است، در یادداشت یا پیام تراکنش وارد کنید. مطمئن شوید که مبلغ زیر با آنچه واقعاً در بانک خود سپرده‌اید مطابقت داشته باشد و شناسه تراکنش ارائه شده توسط بانکتان پس از انجام تراکنش را نیز درج کنید", - "DEPOSIT_BANK_TEXT": ".برای واریز وجه خود از مشخصات بانکی زیر استفاده کنید", - "MIN_DEPOSIT": "کمترین واریزی", - "MAX_DEPOSIT": "بیشترین واریزی", - "BACK": "بازگشت", - "DONE": "انجام شد", - "PENDING_WITHDRAWAL_TITLE": "برداشت در حال انتظار", - "PENDING_WITHDRAWAL_TEXT_1": ".برداشت شما اکنون در مرحله تأیید است و تا زمانی که انجام نشود در حالت معلق باقی می ماند", - "DEPOSIT_AMOUNT_MIN_VALIDATION": ".تراکنش برای ارسال بسیار کوچک است. مقدار بیشتری را امتحان کنید", - "DEPOSIT_AMOUNT_MAX_VALIDATION": ".تراکنش برای ارسال بسیار بزرگ است. مقدار کمتری را امتحان کنید", - "ACCOUNT_NAME": "نام حساب", - "ACCOUNT_NUMBER": "شماره حساب", - "BANK_NAME": "نام بانک", - "VERIFY_BANK_WITHDRAW": ".به منظور برداشت، باید تأییدیه خود را تکمیل کنید که شامل تأیید جزئیات بانکی شما می شود. لطفا روی دکمه ادامه زیر کلیک کنید", - "VERIFICATION_TITLE": "اتمام تایید", - "WITHDRAW_NOTE": ".لطفا توجه داشته باشید: شما فقط می توانید از حسابی به نام خود برداشت کنید", - "USER_PAYMENT": { "TITLE": "پرداخت ها" }, - "QUOTE_CONFIRMATION_MSG_TEXT_1": ".لطفا سفارش خود را بررسی کنید و آن را در قسمت زیر تایید کنید", - "QUOTE_CONFIRMATION_MSG_TEXT_2": ".مبلغ دریافتی، تخمینی است و شامل کارمزد معامله نمی شود", - "ORDER_EXPIRED_MSG": "!سفارش منقضی شده است. لطفا رفرش کنید", - "WITHDRAWALS_FORM_METHOD": "روش", - "WITHDRAWALS_FORM_ADDRESS_EXCHANGE": "ایمیل کاربر صرافی", - "WITHDRAWALS_FORM_EXCHANGE_PLACEHOLDER": "ایمیل کاربر را در این صرافی وارد کنید", - "WITHDRAWALS_FORM_MAIL_INFO": ".ایمیل کاربری ثبت شده در هلاکس را وارد کنید، و به طور رایگان اقدام به انتقال ارز نمایید" -} diff --git a/web/package-lock.json b/web/package-lock.json index 4a7de95ff4..9a1cd35904 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,6 +1,6 @@ { "name": "hollaex-kit", - "version": "2.10.4", + "version": "2.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/web/package.json b/web/package.json index 5cc72a85de..6845ab6aa4 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "hollaex-kit", - "version": "2.10.4", + "version": "2.11.0", "private": true, "dependencies": { "@ant-design/compatible": "1.0.5", diff --git a/web/public/assets/images/Group 5483.svg b/web/public/assets/images/Group 5483.svg new file mode 100644 index 0000000000..00e38dabec --- /dev/null +++ b/web/public/assets/images/Group 5483.svg @@ -0,0 +1 @@ ++ \ No newline at end of file diff --git a/web/public/assets/images/deposit-box.svg b/web/public/assets/images/deposit-box.svg new file mode 100644 index 0000000000..aa12c62974 --- /dev/null +++ b/web/public/assets/images/deposit-box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/assets/images/mini-qr-code.svg b/web/public/assets/images/mini-qr-code.svg index 7004e06ccf..1961154a4e 100644 --- a/web/public/assets/images/mini-qr-code.svg +++ b/web/public/assets/images/mini-qr-code.svg @@ -1,3 +1,3 @@ - + diff --git a/web/public/assets/images/p2p-feature.svg b/web/public/assets/images/p2p-feature.svg new file mode 100644 index 0000000000..becee1358d --- /dev/null +++ b/web/public/assets/images/p2p-feature.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/web/public/assets/images/referral-link-dollar-graphic.svg b/web/public/assets/images/referral-link-dollar-graphic.svg new file mode 100644 index 0000000000..cc5739c39d --- /dev/null +++ b/web/public/assets/images/referral-link-dollar-graphic.svg @@ -0,0 +1,6 @@ + + + $ + + + diff --git a/web/public/assets/images/referrals-icon.svg b/web/public/assets/images/referrals-icon.svg new file mode 100644 index 0000000000..b0dce1c445 --- /dev/null +++ b/web/public/assets/images/referrals-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/assets/images/withdraw-out-box.svg b/web/public/assets/images/withdraw-out-box.svg new file mode 100644 index 0000000000..0b77ce2974 --- /dev/null +++ b/web/public/assets/images/withdraw-out-box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/actions/appActions.js b/web/src/actions/appActions.js index 75605f6350..5ab988601a 100644 --- a/web/src/actions/appActions.js +++ b/web/src/actions/appActions.js @@ -98,6 +98,20 @@ export const SET_ADMIN_WALLET_SORT = 'SET_ADMIN_WALLET_SORT'; export const SET_ADMIN_DIGITAL_ASSETS_SORT = 'SET_ADMIN_DIGITAL_ASSETS_SORT'; export const SET_SELECTED_ACCOUNT = 'SET_SELECTED_ACCOUNT'; export const SET_SELECTED_STEP = 'SET_SELECTED_STEP'; +export const SET_WITHDRAW_CURRENCY = 'SET_WITHDRAW_CURRENCY'; +export const SET_WITHDRAW_NETWORK = 'SET_WITHDRAW_NETWORK'; +export const SET_WITHDRAW_NETWORK_OPTIONS = 'SET_WITHDRAW_NETWORK_OPTIONS'; +export const SET_WITHDRAW_ADDRESS = 'SET_WITHDRAW_ADDRESS'; +export const SET_WITHDRAW_AMOUNT = 'SET_WITHDRAW_AMOUNT'; +export const SET_WITHDRAW_FEE = 'SET_WITHDRAW_FEE'; +export const SET_DEPOSIT_AND_WITHDRAW = 'SET_DEPOSIT_AND_WITHDRAW'; +export const SET_VALID_ADDRESS = 'SET_VALID_ADDRESS'; +export const SET_DEPOSIT_CURRENCY = 'SET_DEPOSIT_CURRENCY'; +export const SET_DEPOSIT_NETWORK = 'SET_DEPOSIT_NETWORK'; +export const SET_DEPOSIT_NETWORK_OPTIONS = 'SET_DEPOSIT_OPTIONS'; +export const SET_SELECTED_METHOD = 'SET_SELECTED_METHOD'; +export const SET_RECEIVER_EMAIL = 'SET_RECEIVER_EMAIL'; +export const SET_WITHDRAW_OTIONAL_TAG = 'SET_WITHDRAW_OTIONAL_TAG'; export const SORT = { VOL: 'volume', @@ -675,3 +689,73 @@ export const getWithdrawalMax = (currency, network) => { `/user/withdrawal/max?currency=${currency}&network=${network}` ); }; + +export const withdrawCurrency = (currency) => ({ + type: SET_WITHDRAW_CURRENCY, + payload: currency, +}); + +export const withdrawNetwork = (network) => ({ + type: SET_WITHDRAW_NETWORK, + payload: network, +}); + +export const withdrawNetworkOptions = (networkOptions) => ({ + type: SET_WITHDRAW_NETWORK_OPTIONS, + payload: networkOptions, +}); + +export const withdrawAddress = (address) => ({ + type: SET_WITHDRAW_ADDRESS, + payload: address, +}); + +export const withdrawAmount = (amount) => ({ + type: SET_WITHDRAW_AMOUNT, + payload: amount, +}); + +export const setFee = (amount) => ({ + type: SET_WITHDRAW_FEE, + payload: amount, +}); + +export const setDepositAndWithdraw = (val) => ({ + type: SET_DEPOSIT_AND_WITHDRAW, + payload: val, +}); + +export const setIsValidAdress = (val) => ({ + type: SET_VALID_ADDRESS, + payload: val, +}); + +export const depositCurrency = (currency) => ({ + type: SET_DEPOSIT_CURRENCY, + payload: currency, +}); + +export const depositNetwork = (network) => ({ + type: SET_DEPOSIT_NETWORK, + payload: network, +}); + +export const depositNetworkOptions = (networkOptions) => ({ + type: SET_DEPOSIT_NETWORK_OPTIONS, + payload: networkOptions, +}); + +export const setSelectedMethod = (method) => ({ + type: SET_SELECTED_METHOD, + payload: method, +}); + +export const setReceiverEmail = (email) => ({ + type: SET_RECEIVER_EMAIL, + payload: email, +}); + +export const setWithdrawOptionaltag = (tag) => ({ + type: SET_WITHDRAW_OTIONAL_TAG, + payload: tag, +}); diff --git a/web/src/actions/authAction.js b/web/src/actions/authAction.js index e2e1e55e48..54559534a6 100644 --- a/web/src/actions/authAction.js +++ b/web/src/actions/authAction.js @@ -86,30 +86,11 @@ const clearTokenInApp = (router, path = '/') => { export function verifyToken(token) { return (dispatch) => { dispatch({ type: 'VERIFY_TOKEN_PENDING' }); - axios({ - method: 'GET', - url: '/verify-token', - headers: { - Authorization: `Bearer ${token}`, - }, - }) - .then((response) => { - setTokenInApp(token); - dispatch({ - type: 'VERIFY_TOKEN_FULFILLED', - payload: token, - }); - }) - .catch((error) => { - const message = error.response - ? error.response.data.message - : 'Invalid token'; - logout(message)(dispatch); - dispatch({ - type: 'VERIFY_TOKEN_REJECTED', - }); - clearTokenInApp(browserHistory, '/login'); - }); + setTokenInApp(token); + dispatch({ + type: 'VERIFY_TOKEN_FULFILLED', + payload: token, + }); }; } diff --git a/web/src/actions/walletActions.js b/web/src/actions/walletActions.js index 3ad5dfef2c..80e16c8c3b 100644 --- a/web/src/actions/walletActions.js +++ b/web/src/actions/walletActions.js @@ -461,3 +461,10 @@ export const getEstimatedDust = (assets = []) => { const data = { assets }; return axios.post(ENDPOINTS.DUST_ESTIMATION, data); }; + +export const activeTabFromWallet = (tab) => ({ + type: 'ACTIVE_TAB_FROM_WALLET', + payload: { + tab, + }, +}); diff --git a/web/src/components/Accordion/_Accordion.scss b/web/src/components/Accordion/_Accordion.scss index 221fcb972f..07631a9c77 100644 --- a/web/src/components/Accordion/_Accordion.scss +++ b/web/src/components/Accordion/_Accordion.scss @@ -16,6 +16,7 @@ $main-text--color: $colors-deactivate-color2; } .accordion_wrapper { .accordion_section { + border-bottom: unset !important; .accordion_section_title { font-size: 1.75rem; } diff --git a/web/src/components/ActionNotification/index.js b/web/src/components/ActionNotification/index.js index 6aa7086c5a..2eeb0854d6 100644 --- a/web/src/components/ActionNotification/index.js +++ b/web/src/components/ActionNotification/index.js @@ -2,6 +2,7 @@ import React from 'react'; import classnames from 'classnames'; import Image from 'components/Image'; import { isMobile } from 'react-device-detect'; +import { MoreOutlined } from '@ant-design/icons'; const getClassNames = (status) => { switch (status) { @@ -39,7 +40,9 @@ const ActionNotification = ({ showActionText, hideActionText = false, disable = false, + isFromWallet = false, }) => { + const isVisibale = isFromWallet ? isFromWallet : !isMobile; // This is to prevent action when edit string or upload icons are clicked const onActionClick = ({ target: { dataset = {} } }) => { const { stringId, iconId } = dataset; @@ -69,29 +72,35 @@ const ActionNotification = ({ )} onClick={onActionClick} > - {!hideActionText && (showActionText || !isMobile) && ( + {!hideActionText && (showActionText || isVisibale) && (
- {text} + {text === 'mobile-trade' ? ( + + ) : ( + text + )}
)} - {text} + {text !== 'mobile-trade' && ( + {text} + )} ); }; diff --git a/web/src/components/AppBar/_AppBar.scss b/web/src/components/AppBar/_AppBar.scss index 504501e153..0431f2b232 100644 --- a/web/src/components/AppBar/_AppBar.scss +++ b/web/src/components/AppBar/_AppBar.scss @@ -135,6 +135,23 @@ $app-menu-width: calc(100vw - 40rem); stroke: $base_top-bar-navigation_text-inactive; } } + + .app-bar-deposit-btn { + color: $app-bar-icon-inactive; + background-color: $link; + padding: 2px 10px 0px 10px; + border-radius: 3px; + cursor: pointer; + font-size: 11px; + .margin-aligner { + margin-bottom: unset; + margin-right: unset; + svg { + width: 15px !important; + height: 15px !important; + } + } + } } .app_bar-pair-overflow { @@ -1126,7 +1143,7 @@ $app-menu-width: calc(100vw - 40rem); // } .app-menu-bar-content { - width: 12rem; + width: 8rem; } } .app-bar-add-tab-menu { diff --git a/web/src/components/AppBar/index.js b/web/src/components/AppBar/index.js index 6a79c63b07..f730f32e21 100644 --- a/web/src/components/AppBar/index.js +++ b/web/src/components/AppBar/index.js @@ -8,7 +8,12 @@ import { DEFAULT_URL } from 'config/constants'; import MenuList from './MenuList'; import { MobileBarWrapper, EditWrapper, ButtonLink, Image } from 'components'; import { isLoggedIn } from 'utils/token'; -import { getTickers, changeTheme, setLanguage } from 'actions/appActions'; +import { + getTickers, + changeTheme, + setLanguage, + setDepositAndWithdraw, +} from 'actions/appActions'; import { updateUserSettings, setUserData } from 'actions/userAction'; import ThemeSwitcher from './ThemeSwitcher'; import withEdit from 'components/EditProvider/withEdit'; @@ -216,6 +221,12 @@ class AppBar extends Component { ); }; + onHandleDeposit = () => { + const { setDepositAndWithdraw, router } = this.props; + setDepositAndWithdraw(true); + router.push('/wallet/deposit'); + }; + render() { const { user, @@ -229,6 +240,7 @@ class AppBar extends Component { isHome, activeLanguage, changeLanguage, + icons, } = this.props; const { securityPending, verificationPending, walletPending } = this.state; @@ -304,6 +316,17 @@ class AppBar extends Component { id="trade-nav-container" className="d-flex app-bar-account justify-content-end" > +
+ + {STRINGS['ACCORDIAN.DEPOSIT_LABEL']} +
({ changeTheme: bindActionCreators(changeTheme, dispatch), setUserData: bindActionCreators(setUserData, dispatch), changeLanguage: bindActionCreators(setLanguage, dispatch), + setDepositAndWithdraw: bindActionCreators(setDepositAndWithdraw, dispatch), }); export default connect( diff --git a/web/src/components/ConfigProvider/index.js b/web/src/components/ConfigProvider/index.js index fab12f0f5a..ed3dae32a4 100644 --- a/web/src/components/ConfigProvider/index.js +++ b/web/src/components/ConfigProvider/index.js @@ -42,7 +42,6 @@ class ConfigProvider extends Component { const script = document.createElement('script'); script.src = `https://www.google.com/recaptcha/api.js?render=${DEFAULT_CAPTCHA_SITEKEY}`; document.body.appendChild(script); - } UNSAFE_componentWillUpdate(_, nextState) { diff --git a/web/src/components/Form/FormFields/_FieldWrapper.scss b/web/src/components/Form/FormFields/_FieldWrapper.scss index f6ce550dc8..2cbae261ab 100644 --- a/web/src/components/Form/FormFields/_FieldWrapper.scss +++ b/web/src/components/Form/FormFields/_FieldWrapper.scss @@ -335,6 +335,8 @@ $outline-height: 1.5px; .field-children { .clear-field { bottom: 1.5rem; + padding: 1.5rem; + margin-right: 1rem; } } } diff --git a/web/src/components/Form/validations.js b/web/src/components/Form/validations.js index fd77274a3f..e915207a60 100644 --- a/web/src/components/Form/validations.js +++ b/web/src/components/Form/validations.js @@ -37,7 +37,7 @@ export const passwordsMatch = (value, allValues) => export const username = (value = '') => !usernameRegEx.test(value) ? STRINGS['INVALID_USERNAME'] : undefined; -export const validAddress = (symbol = '', message, network) => { +export const validAddress = (symbol = '', message, network, key = '') => { let currency = network ? network.toUpperCase() : symbol.toUpperCase(); return (address) => { let valid; @@ -48,7 +48,10 @@ export const validAddress = (symbol = '', message, network) => { const supported = WAValidator.findCurrency(currency); if (supported) { - valid = WAValidator.validate(address, currency); + valid = WAValidator.validate( + address ? address : (address = key), + currency + ); } else { valid = true; } diff --git a/web/src/components/Notification/Deposit.js b/web/src/components/Notification/Deposit.js index 9240dea9d8..d1bd198d4c 100644 --- a/web/src/components/Notification/Deposit.js +++ b/web/src/components/Notification/Deposit.js @@ -1,5 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; +import { browserHistory } from 'react-router'; import classnames from 'classnames'; import { CurrencyBallWithPrice, ActionNotification, Button } from 'components'; @@ -40,6 +41,12 @@ const DepositNotification = ({ onClose(); openContactForm(); }; + + const onHandleDeposit = () => { + onClose(); + browserHistory.push('/transactions?tab=2'); + }; + return (
@@ -67,9 +74,15 @@ const DepositNotification = ({
+
diff --git a/web/src/components/Notification/NewOrder.js b/web/src/components/Notification/NewOrder.js index e4716c7ff8..059d5576c2 100644 --- a/web/src/components/Notification/NewOrder.js +++ b/web/src/components/Notification/NewOrder.js @@ -86,14 +86,13 @@ const NewOrderNotification = ({ return ( <> - - + <> @@ -103,9 +102,8 @@ const NewOrderNotification = ({ -
- {showMintAndBurnButtons && ( - - -
- - - )} -
+
+
+ {showConfigureButton && ( + + )} +
+ {showMintAndBurnButtons && ( + + +
+ + + )}
- )} +
); } diff --git a/web/src/containers/Admin/AppWrapper/index.js b/web/src/containers/Admin/AppWrapper/index.js index 462d0287f8..1efe8c3440 100644 --- a/web/src/containers/Admin/AppWrapper/index.js +++ b/web/src/containers/Admin/AppWrapper/index.js @@ -16,10 +16,8 @@ import { isSupport, isSupervisor, isAdmin, - getTokenTimestamp, checkRole, } from 'utils/token'; -import { checkUserSessionExpired } from 'utils/utils'; import { getExchangeInitialized, getSetupCompleted } from 'utils/initialize'; import { logout } from 'actions/authAction'; import { getMe, setMe } from 'actions/userAction'; @@ -106,12 +104,6 @@ class AppWrapper extends React.Component { }; } - UNSAFE_componentWillMount() { - if (isLoggedIn() && checkUserSessionExpired(getTokenTimestamp())) { - this.logout('Token is expired'); - } - } - componentDidMount() { this.getData(); // this.getAssets(); diff --git a/web/src/containers/Admin/Billing/generalContent.js b/web/src/containers/Admin/Billing/generalContent.js index 1175037c78..8b162b88a8 100644 --- a/web/src/containers/Admin/Billing/generalContent.js +++ b/web/src/containers/Admin/Billing/generalContent.js @@ -658,6 +658,8 @@ const GeneralContent = ({ > {selectedType === 'diy' ? 'Do-It-Yourself' + : selectedType === 'fiat' + ? 'Enterprise' : selectedType}

@@ -704,10 +706,7 @@ const GeneralContent = ({ }; const checkDisabled = (method) => { - if ( - method === 'bank' || - (selectedType === 'basic' && method === 'paypal') - ) { + if (method === 'bank' || method === 'paypal') { return true; } return false; @@ -862,7 +861,7 @@ const GeneralContent = ({ {opt.method === 'cryptoCurrency' ? ( <> {opt.label} - (up to 10% off) + (up to 5% off) { + handleSaveInterface = ( + features, + balance_history_config = null, + referral_history_config = null + ) => { this.handleSubmitGeneral({ kit: { features, balance_history_config, + referral_history_config, }, }); }; @@ -754,7 +759,13 @@ class GeneralContent extends Component { defaultEmailData, } = this.state; const { kit = {} } = this.state.constants; - const { coins, themeOptions, activeTab, handleTabChange } = this.props; + const { + coins, + themeOptions, + activeTab, + handleTabChange, + enabledPlugins, + } = this.props; const generalFields = getGeneralFields(coins); if (loading) { @@ -1092,6 +1103,7 @@ class GeneralContent extends Component { buttonSubmitting={buttonSubmitting} isFiatUpgrade={isFiatUpgrade} coins={coins} + enabledPlugins={enabledPlugins} /> ) : null} {activeTab === 'security' ? ( @@ -1226,6 +1238,7 @@ const mapStateToProps = (state) => ({ tokens: state.user.tokens, user: state.user, constants: state.app.constants, + enabledPlugins: state.app.enabledPlugins, }); const mapDispatchToProps = (dispatch) => ({ diff --git a/web/src/containers/Admin/General/InterfaceForm.js b/web/src/containers/Admin/General/InterfaceForm.js index 1f3ad1e96e..0d9c7ebcf4 100644 --- a/web/src/containers/Admin/General/InterfaceForm.js +++ b/web/src/containers/Admin/General/InterfaceForm.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { ReactSVG } from 'react-svg'; -import { Button, Checkbox, Form, Modal, Select, message } from 'antd'; +import { Button, Checkbox, Form, Input, Modal, Select, message } from 'antd'; import classnames from 'classnames'; import _isEqual from 'lodash/isEqual'; @@ -17,6 +17,7 @@ const InterfaceForm = ({ buttonSubmitting, isFiatUpgrade, coins, + enabledPlugins, }) => { const [isSubmit, setIsSubmit] = useState(!buttonSubmitting); const [form] = Form.useForm(); @@ -30,6 +31,25 @@ const InterfaceForm = ({ false ); + const [referralHistoryData, setReferralHistoryData] = useState({ + currency: constants?.kit?.referral_history_config?.currency || 'usdt', + earning_rate: constants?.kit?.referral_history_config?.earning_rate || 0, + minimum_amount: + constants?.kit?.referral_history_config?.minimum_amount || 1, + earning_period: + constants?.kit?.referral_history_config?.earning_period || 0, + distributor_id: + constants?.kit?.referral_history_config?.distributor_id || null, + date_enabled: + constants?.kit?.referral_history_config?.date_enabled || new Date(), + active: constants?.kit?.referral_history_config?.active, + }); + + const [ + displayReferralHistoryModal, + setDisplayReferralHistoryModal, + ] = useState(false); + const handleSubmit = (values) => { let formValues = {}; if (values) { @@ -40,6 +60,7 @@ const InterfaceForm = ({ stake_page: !!values.stake_page, cefi_stake: !!values.cefi_stake, balance_history_config: !!values.balance_history_config, + referral_history_config: !!values.referral_history_config, home_page: isUpgrade ? false : !!values.home_page, ultimate_fiat: !!values.ultimate_fiat, apps: !!values.apps, @@ -49,7 +70,20 @@ const InterfaceForm = ({ active: !!values.balance_history_config || false, date_enabled: balanceHistoryCurrency.date_enabled, }; - handleSaveInterface(formValues, balance_history_config); + const referral_history_config = { + active: !!values.referral_history_config, + currency: referralHistoryData.currency, + earning_rate: Number(referralHistoryData.earning_rate), + minimum_amount: Number(referralHistoryData.minimum_amount), + earning_period: Number(referralHistoryData.earning_period), + distributor_id: Number(referralHistoryData.distributor_id), + date_enabled: referralHistoryData.date_enabled, + }; + handleSaveInterface( + formValues, + balance_history_config, + referral_history_config + ); } }; @@ -62,7 +96,22 @@ const InterfaceForm = ({ }; const handleSubmitData = (formProps) => { - if (formProps.balance_history_config && !balanceHistoryCurrency.currency) { + if (formProps.referral_history_config && !referralHistoryData.active) { + if ( + enabledPlugins.includes('referral') && + !enabledPlugins.includes('referral-migrate') + ) { + message.error( + 'In order to use the Referral System feature, you have to install Referral Migrate plugin to migrate the necessary data from the existing referral plugin to the new system.', + 10 + ); + } else { + setDisplayReferralHistoryModal(true); + } + } else if ( + formProps.balance_history_config && + !balanceHistoryCurrency.currency + ) { setDisplayBalanceHistoryModal(true); } else { setIsSubmit(true); @@ -104,8 +153,8 @@ const InterfaceForm = ({
This currency is used as the base currency to calculate and display all the profits and loss. It is normally set to a fiat - currency or a stable coin. - Note that this currency can not be modified in future after it starts getting the information. + currency or a stable coin. Note that this currency can not be + modified in future after it starts getting the information.
{ + setReferralHistoryData({ + ...referralHistoryData, + currency: e, + }); + }} + > + {Object.keys(coins).map((key) => ( + {coins[key].fullname} + ))} + +
+ +
+
+ Earning Rate +
+ Earning rate referee users receive from affiliated users fees +
+
+ + +
+ +
+
+ Earning Period +
+ Number of months referee users earn affiliated users fees. Set + to 0 for no earning expiry +
+
+ + { + setReferralHistoryData({ + ...referralHistoryData, + earning_period: Number(e.target.value), + }); + }} + /> +
+
+
+ Minimum amount +
+ Minimum amount reqired to settle fees +
+
+ + { + setReferralHistoryData({ + ...referralHistoryData, + minimum_amount: Number(e.target.value), + }); + }} + /> +
+ +
+
+ Distributor ID +
+ Account ID to send settled fees from +
+
+ + { + setReferralHistoryData({ + ...referralHistoryData, + distributor_id: Number(e.target.value), + }); + }} + /> +
+ +
+ + +
+ + )} +
Features
Select the features that will be available on your exchange. @@ -289,6 +521,56 @@ const InterfaceForm = ({ )} + {!isFiatUpgrade && ( + + +
+ + + +
+ Referral System{' '} + {referralHistoryData.active && ( + { + e.stopPropagation(); + e.preventDefault(); + setDisplayReferralHistoryModal(true); + }} + > + Configure + + )} +
+ (User referral system with analytics) +
+
+
+
+
+ )} +

- Website + + Website +

{free_for?.length ? (
diff --git a/web/src/containers/Admin/Settings/Utils.js b/web/src/containers/Admin/Settings/Utils.js index fc5a0ee28f..1f0cfb2fec 100644 --- a/web/src/containers/Admin/Settings/Utils.js +++ b/web/src/containers/Admin/Settings/Utils.js @@ -161,7 +161,7 @@ export const generateAdminSettings = (key) => { label: 'Allowed domains', placeholder: 'Allowed domains', tokenSeparators: [',', ' ', ' '], - } + }, }; } else if (key === 'email') { return { diff --git a/web/src/containers/Admin/Trades/actions.js b/web/src/containers/Admin/Trades/actions.js index ccf769a89e..113d41d5fe 100644 --- a/web/src/containers/Admin/Trades/actions.js +++ b/web/src/containers/Admin/Trades/actions.js @@ -2,6 +2,23 @@ import axios from 'axios'; import querystring from 'query-string'; import { requestAuthenticated } from '../../../utils'; +const toQueryString = (values) => { + return querystring.stringify(values); +}; + +export const requestDisputes = (values) => { + const queryValues = + values && Object.keys(values).length ? querystring.stringify(values) : ''; + return requestAuthenticated(`/admin/p2p/dispute?${queryValues}`); +}; +export const editDispute = (values) => { + const options = { + method: 'PUT', + body: JSON.stringify(values), + }; + + return requestAuthenticated('/admin/p2p/dispute', options); +}; export const requestTrades = (id) => { const query = querystring.stringify({ @@ -101,6 +118,14 @@ export const updateConstants = (values) => { export const getBrokerQuote = (symbol, side) => requestAuthenticated(`/broker/quote?symbol=${symbol}&side=${side}`); +export const requestUsers = (values) => { + let url = '/admin/users'; + if (values) { + url = `/admin/users?${toQueryString(values)}`; + } + return requestAuthenticated(url); +}; + export const getBrokerConnect = ( exchange_id, api_key, @@ -115,3 +140,9 @@ export const getBrokerConnect = ( return requestAuthenticated(urlString); }; + +export const requestDeals = (values) => { + const queryValues = + values && Object.keys(values).length ? querystring.stringify(values) : ''; + return requestAuthenticated(`/p2p/deal?${queryValues}`); +}; diff --git a/web/src/containers/Admin/Trades/index.js b/web/src/containers/Admin/Trades/index.js index 8b0d24cb04..c46ccbb49d 100644 --- a/web/src/containers/Admin/Trades/index.js +++ b/web/src/containers/Admin/Trades/index.js @@ -10,6 +10,7 @@ import { setExchange } from 'actions/assetActions'; import OtcDeskContainer from './otcdesk'; import QuickTradeTab from './QuickTradeConfig'; import ExchangeOrdersContainer from '../Orders'; +import P2P from './p2p'; import './index.css'; const TabPane = Tabs.TabPane; @@ -108,7 +109,22 @@ const PairsTab = (props) => { balanceData={props.user && props.user.balance} /> - + + + + { + const [hideTabs, setHideTabs] = useState(false); + const [activeTab, setActiveTab] = useState('1'); + + const handleTabChange = (key) => { + setActiveTab(key); + }; + + const renderTabBar = (props, DefaultTabBar) => { + if (hideTabs) return
; + return ; + }; + + return ( +
+ + + + + + + + + + + +
+ ); +}; + +const mapStateToProps = (state) => ({ + exchange: state.asset && state.asset.exchange, + coins: state.asset.allCoins, + pairs: state.asset.allPairs, + user: state.user, + quicktrade: state.app.allContracts.quicktrade, + networkQuickTrades: state.app.allContracts.networkQuickTrades, + coinObjects: state.app.allContracts.coins, + broker: state.app.broker, + features: state.app.constants.features, +}); + +const mapDispatchToProps = (dispatch) => ({ + setExchange: bindActionCreators(setExchange, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(P2P); diff --git a/web/src/containers/Admin/Trades/p2pDeals.js b/web/src/containers/Admin/Trades/p2pDeals.js new file mode 100644 index 0000000000..4fa64484e1 --- /dev/null +++ b/web/src/containers/Admin/Trades/p2pDeals.js @@ -0,0 +1,341 @@ +/* eslint-disable */ +import React, { useState, useEffect } from 'react'; +import { Table, Button, Spin, Input } from 'antd'; +import { requestDeals } from './actions'; +import moment from 'moment'; +import BigNumber from 'bignumber.js'; +import { ExclamationCircleFilled } from '@ant-design/icons'; +import { connect } from 'react-redux'; +import { CloseOutlined } from '@ant-design/icons'; +const P2PDeals = ({ coins }) => { + const [userData, setUserData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [queryValues, setQueryValues] = useState(); + const [queryFilters, setQueryFilters] = useState({ + total: 0, + page: 1, + pageSize: 10, + limit: 50, + currentTablePage: 1, + isRemaining: true, + }); + + const [userQuery, setUserQuery] = useState({}); + + const statuses = { + staking: 2, + unstaking: 1, + closed: 3, + }; + + const columns = [ + { + title: 'Vendor ID', + dataIndex: 'merchant_id', + key: 'merchant_id', + render: (user_id, data) => { + return ( +
+ +
+ ); + }, + }, + { + title: 'Price', + dataIndex: 'price', + key: 'price', + render: (user_id, data) => { + return ( +
+ {data.exchange_rate * (1 + Number(data.spread || 0))}{' '} + {data.spending_asset.toUpperCase()} +
+ ); + }, + }, + { + title: 'Limit/Available', + dataIndex: 'limit', + key: 'limit', + render: (user_id, data) => { + return ( +
+
+ Available: {data.total_order_amount}{' '} + {data.buying_asset.toUpperCase()} + {','} +
+
+ Limit: {data.min_order_value} - {data.max_order_value}{' '} + {data.spending_asset.toUpperCase()} +
+
+ ); + }, + }, + { + title: 'Methods', + dataIndex: 'methods', + key: 'methods', + render: (user_id, data) => { + return ( +
+ {data.payment_methods + .map((method) => method.system_name) + .join(', ')} +
+ ); + }, + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: (user_id, data) => { + return ( +
{data?.status ? 'Active' : 'Inactive'}
+ ); + }, + }, + { + title: 'Start date', + dataIndex: 'created_at', + key: 'created_at', + render: (user_id, data) => { + return
{formatDate(data?.created_at)}
; + }, + }, + + // { + // title: 'Action', + // dataIndex: '', + // key: '', + // render: (user_id, data) => { + // return ( + //
+ // + + //
+ // ); + // }, + // }, + ]; + + useEffect(() => { + requestExchangeStakers(queryFilters.page, queryFilters.limit); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [queryValues]); + + const formatDate = (date) => { + return moment(date).format('DD/MMM/YYYY, hh:mmA ').toUpperCase(); + }; + + const requestExchangeStakers = (page = 1, limit = 50) => { + setIsLoading(true); + requestDeals({ page, limit, ...queryValues }) + .then((response) => { + setUserData( + page === 1 ? response.data : [...userData, ...response.data] + ); + + setQueryFilters({ + total: response.count, + fetched: true, + page, + currentTablePage: page === 1 ? 1 : queryFilters.currentTablePage, + isRemaining: response.count > page * limit, + }); + + setIsLoading(false); + }) + .catch((error) => { + // const message = error.message; + setIsLoading(false); + }); + }; + + const pageChange = (count, pageSize) => { + const { page, limit, isRemaining } = queryFilters; + const pageCount = count % 5 === 0 ? 5 : count % 5; + const apiPageTemp = Math.floor(count / 5); + if (limit === pageSize * pageCount && apiPageTemp >= page && isRemaining) { + requestExchangeStakers(page + 1, limit); + } + setQueryFilters({ ...queryFilters, currentTablePage: count }); + }; + + return ( +
+
P2p Deals
+
Track p2p deals on the exchange
+ +
+
+ {/* { + setQueryValues(filters); + }} + fieldKeyValue={fieldKeyValue} + defaultFilters={defaultFilters} + /> */} +
+
+
+ {/* { + requestDownload(); + }} + className="mb-2 underline-text cursor-pointer" + style={{ cursor: 'pointer' }} + > + Search user + */} + +
+
Search vendor
+
+ { + setUserQuery({ + ...(userQuery?.status && { status: userQuery.status }), + ...(e.target.value && { user_id: e.target.value }), + }); + }} + value={userQuery.user_id} + /> + +
+
+
+ +
+ {/* + + */} + {/* Total: {queryFilters.total || '-'} */} +
+ Total disputes:{' '} + {queryFilters.total} +
+ +
-
+
+
+ + {/*
+ + Insufficient funds to settle! Fill the source ABC wallet account: + User 1 (operator@account.com) + + + + +
*/} + +
+ + { + return statuses[a.status] - statuses[b.status]; + }) + .filter((x) => + userQuery?.status === 'closed' + ? x.status === 'closed' + : x.status !== 'closed' + )} + // expandedRowRender={renderRowContent} + expandRowByClick={true} + rowKey={(data) => { + return data.id; + }} + pagination={{ + current: queryFilters.currentTablePage, + onChange: pageChange, + }} + /> + + + + + + ); +}; + +const mapStateToProps = (state) => ({ + exchange: state.asset && state.asset.exchange, + coins: state.asset.allCoins, + pairs: state.asset.allPairs, + user: state.user, + quicktrade: state.app.allContracts.quicktrade, + networkQuickTrades: state.app.allContracts.networkQuickTrades, + coinObjects: state.app.allContracts.coins, + broker: state.app.broker, + features: state.app.constants.features, +}); + +const mapDispatchToProps = (dispatch) => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(P2PDeals); diff --git a/web/src/containers/Admin/Trades/p2pDisputes.js b/web/src/containers/Admin/Trades/p2pDisputes.js new file mode 100644 index 0000000000..c3c47ef823 --- /dev/null +++ b/web/src/containers/Admin/Trades/p2pDisputes.js @@ -0,0 +1,423 @@ +import React, { useState, useEffect } from 'react'; +import { Table, Button, Spin, Input, Modal, message } from 'antd'; +import { requestDisputes, editDispute } from './actions'; +import moment from 'moment'; +// import BigNumber from 'bignumber.js'; +// import { ExclamationCircleFilled } from '@ant-design/icons'; +import { connect } from 'react-redux'; +import { CloseOutlined } from '@ant-design/icons'; +import { Link } from 'react-router'; + +const P2PDisputes = ({ coins }) => { + const [userData, setUserData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [queryValues, setQueryValues] = useState(); + const [queryFilters, setQueryFilters] = useState({ + total: 0, + page: 1, + pageSize: 10, + limit: 50, + currentTablePage: 1, + isRemaining: true, + }); + + const [userQuery, setUserQuery] = useState({}); + const [resolution, setResolution] = useState(); + const [displayAdjudicate, setDisplayAdjudicate] = useState(false); + const [selectedDispute, setSelectedDispute] = useState(); + const statuses = { + staking: 2, + unstaking: 1, + closed: 3, + }; + + const columns = [ + { + title: 'Initiator Id', + dataIndex: 'initiator_id', + key: 'initiator_id', + render: (user_id, data) => { + return ( +
+ +
+ ); + }, + }, + { + title: 'Defendant Id', + dataIndex: 'defendant_id', + key: 'defendant_id', + render: (user_id, data) => { + return ( +
+ +
+ ); + }, + }, + { + title: 'User’s Reason', + dataIndex: 'reason', + key: 'reason', + render: (user_id, data) => { + return ( +
+ {data?.reason?.toUpperCase() || 'No reason specified'} +
+ ); + }, + }, + { + title: 'Resolution', + dataIndex: 'resolution', + key: 'resolution', + render: (user_id, data) => { + return ( +
+ {data?.resolution?.toUpperCase() || 'Not adjudicated'} +
+ ); + }, + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + render: (user_id, data) => { + return ( +
{data?.status ? 'Active' : 'Inactive'}
+ ); + }, + }, + { + title: 'Start date', + dataIndex: 'created_at', + key: 'created_at', + render: (user_id, data) => { + return
{formatDate(data?.created_at)}
; + }, + }, + + { + title: 'Action', + dataIndex: '', + key: '', + render: (user_id, data) => { + return !data.status ? ( +
Closed
+ ) : ( +
+ +
+ ); + }, + }, + ]; + + useEffect(() => { + requestExchangeStakers(queryFilters.page, queryFilters.limit); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [queryValues]); + + const formatDate = (date) => { + return moment(date).format('DD/MMM/YYYY, hh:mmA ').toUpperCase(); + }; + + const requestExchangeStakers = (page = 1, limit = 50) => { + setIsLoading(true); + requestDisputes({ page, limit, ...queryValues }) + .then((response) => { + setUserData( + page === 1 ? response.data : [...userData, ...response.data] + ); + + setQueryFilters({ + total: response.count, + fetched: true, + page, + currentTablePage: page === 1 ? 1 : queryFilters.currentTablePage, + isRemaining: response.count > page * limit, + }); + + setIsLoading(false); + }) + .catch((error) => { + // const message = error.message; + setIsLoading(false); + }); + }; + + const pageChange = (count, pageSize) => { + const { page, limit, isRemaining } = queryFilters; + const pageCount = count % 5 === 0 ? 5 : count % 5; + const apiPageTemp = Math.floor(count / 5); + if (limit === pageSize * pageCount && apiPageTemp >= page && isRemaining) { + requestExchangeStakers(page + 1, limit); + } + setQueryFilters({ ...queryFilters, currentTablePage: count }); + }; + + return ( +
+ } + bodyStyle={{ + backgroundColor: '#27339D', + marginTop: 60, + }} + visible={displayAdjudicate} + width={450} + footer={null} + onCancel={() => { + setDisplayAdjudicate(false); + }} + > +

+ Resolve the dispute +

+
Input resolution
+ { + setResolution(e.target.value); + }} + width={400} + /> + +
+ + +
+
+
P2p Disputes
+
+ Track the users that have active disputes +
+ +
+
+ {/* { + setQueryValues(filters); + }} + fieldKeyValue={fieldKeyValue} + defaultFilters={defaultFilters} + /> */} +
+
+
+ {/* { + requestDownload(); + }} + className="mb-2 underline-text cursor-pointer" + style={{ cursor: 'pointer' }} + > + Search user + */} + +
+
Search user
+
+ { + setUserQuery({ + ...(userQuery?.status && { status: userQuery.status }), + ...(e.target.value && { user_id: e.target.value }), + }); + }} + value={userQuery.user_id} + /> + +
+
+
+ +
+ {/* + + */} + {/* Total: {queryFilters.total || '-'} */} +
+ Total disputes:{' '} + {queryFilters.total} +
+ +
-
+
+
+ + {/*
+ + Insufficient funds to settle! Fill the source ABC wallet account: + User 1 (operator@account.com) + + + + +
*/} + +
+ +
{ + return statuses[a.status] - statuses[b.status]; + }) + .filter((x) => + userQuery?.status === 'closed' + ? x.status === 'closed' + : x.status !== 'closed' + )} + // expandedRowRender={renderRowContent} + expandRowByClick={true} + rowKey={(data) => { + return data.id; + }} + pagination={{ + current: queryFilters.currentTablePage, + onChange: pageChange, + }} + /> + + + + + + ); +}; + +const mapStateToProps = (state) => ({ + exchange: state.asset && state.asset.exchange, + coins: state.asset.allCoins, + pairs: state.asset.allPairs, + user: state.user, + quicktrade: state.app.allContracts.quicktrade, + networkQuickTrades: state.app.allContracts.networkQuickTrades, + coinObjects: state.app.allContracts.coins, + broker: state.app.broker, + features: state.app.constants.features, +}); + +const mapDispatchToProps = (dispatch) => ({}); + +export default connect(mapStateToProps, mapDispatchToProps)(P2PDisputes); diff --git a/web/src/containers/Admin/Trades/p2pSettings.js b/web/src/containers/Admin/Trades/p2pSettings.js new file mode 100644 index 0000000000..2be21e534a --- /dev/null +++ b/web/src/containers/Admin/Trades/p2pSettings.js @@ -0,0 +1,2628 @@ +/* eslint-disable */ +import React, { useEffect, useState, useRef } from 'react'; +import { + Tabs, + message, + Modal, + Button, + Select, + Checkbox, + Input, + Switch, +} from 'antd'; +import { Radio, Space } from 'antd'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { CloseOutlined } from '@ant-design/icons'; +import { setExchange } from 'actions/assetActions'; +import { requestTiers } from '../Tiers/action'; +import { updateConstants, requestUsers } from './actions'; +import { requestAdminData } from 'actions/appActions'; +import _debounce from 'lodash/debounce'; +import Coins from '../Coins'; +import './index.css'; + +const TabPane = Tabs.TabPane; + +const P2PSettings = ({ coins, pairs, p2p_config, features }) => { + const [displayP2pModel, setDisplayP2pModel] = useState(false); + const [displayFiatAdd, setDisplayFiatAdd] = useState(false); + const [displayPaymentAdd, setDisplayPaymentAdd] = useState(false); + const [displayNewPayment, setDisplayNewPayment] = useState(false); + const [paymentFieldAdd, setPaymentFieldAdd] = useState(false); + const [step, setStep] = useState(0); + + const [side, setSide] = useState(); + const [digitalCurrencies, setDigitalCurrencies] = useState([]); + const [fiatCurrencies, setFiatCurrencies] = useState([]); + const [tiers, setTiers] = useState(); + const [merchantTier, setMerchantTier] = useState(); + const [userTier, setUserTier] = useState(); + const [paymentMethod, setPaymentMethod] = useState({ + system_name: null, + fields: {}, + }); + const [customFields, setCustomFields] = useState([ + { + id: 1, + name: null, + required: true, + }, + ]); + const [customField, setCustomField] = useState({ + id: null, + name: null, + required: null, + }); + + const defaultPaymentMethods = [ + { + fields: [ + { + id: 1, + name: 'Bank Number', + required: true, + }, + { + id: 2, + name: 'Full Name', + required: true, + }, + ], + system_name: 'IBAN', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + { + id: 2, + name: 'Routing Number', + required: true, + }, + { + id: 3, + name: 'Account Holder Name', + required: true, + }, + ], + system_name: 'Wire Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + { + id: 2, + name: 'Routing Number', + required: true, + }, + { + id: 3, + name: 'Account Holder Name', + required: true, + }, + ], + system_name: 'ACH Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + { + id: 2, + name: 'SWIFT Code', + required: true, + }, + { + id: 3, + name: 'Account Holder Name', + required: true, + }, + ], + system_name: 'SWIFT Transfer', + }, + { + fields: [ + { + id: 1, + name: 'IBAN', + required: true, + }, + { + id: 2, + name: 'BIC', + required: true, + }, + { + id: 3, + name: 'Account Holder Name', + required: true, + }, + ], + system_name: 'SEPA Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + { + id: 2, + name: 'Currency', + required: true, + }, + ], + system_name: 'Wise', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + { + id: 2, + name: 'Currency', + required: true, + }, + ], + system_name: 'PayPal', + }, + { + fields: [ + { + id: 1, + name: 'Email or Phone', + required: true, + }, + ], + system_name: 'Zelle', + }, + { + fields: [ + { + id: 1, + name: 'Username', + required: true, + }, + ], + system_name: 'Venmo', + }, + { + fields: [ + { + id: 1, + name: 'MTCN', + required: true, + }, + ], + system_name: 'Western Union', + }, + { + fields: [ + { + id: 1, + name: 'Reference Number', + required: true, + }, + ], + system_name: 'MoneyGram', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'Revolut', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'Alipay', + }, + { + fields: [ + { + id: 1, + name: 'WeChat ID', + required: true, + }, + ], + system_name: 'WeChat Pay', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'Square Cash', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'Stripe', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Amazon Pay', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'Payoneer', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Skrill', + }, + { + fields: [ + { + id: 1, + name: 'Account ID', + required: true, + }, + ], + system_name: 'Neteller', + }, + { + fields: [ + { + id: 1, + name: 'Voucher Code', + required: true, + }, + ], + system_name: 'Paysafe', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Klarna', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'Afterpay', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'Bill.com', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'QuickBooks Payments', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'GoCardless', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'Braintree', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'WorldPay', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Adyen', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'BlueSnap', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'Chase QuickPay', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + { + id: 2, + name: 'Routing Number', + required: true, + }, + ], + system_name: 'Citibank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'Barclaycard', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Paysend', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'Monzo', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + { + id: 2, + name: 'Routing Number', + required: true, + }, + ], + system_name: 'HSBC Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'Cash App', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'N26 Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'TransferGo', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'BitPay', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Coinbase', + }, + { + fields: [ + { + id: 1, + name: 'Wallet Address', + required: true, + }, + ], + system_name: 'Blockchain Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + { + id: 2, + name: 'Routing Number', + required: true, + }, + ], + system_name: 'Lloyds Bank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'Revolut Card Payment', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'N26 Card Payment', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'Monzo Card Payment', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'TransferWise Card Payment', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'N26 Bank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Zen Pay', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'Ally Bank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'Bank of America Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + { + id: 2, + name: 'Routing Number', + required: true, + }, + ], + system_name: 'Santander Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'First Direct Card Payment', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'PNC Bank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'USAA Card Payment', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + { + id: 2, + name: 'Routing Number', + required: true, + }, + ], + system_name: 'Chime Bank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'Capital One Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'Fifth Third Bank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'Discover Card Payment', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Netspend', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'N26 Bank Payment', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'M-Pesa', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Payza', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'JCB Card Payment', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'Zenith Bank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'GTBank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Paytm', + }, + { + fields: [ + { + id: 1, + name: 'Account Number', + required: true, + }, + ], + system_name: 'Ecobank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'FirstBank Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Remitly', + }, + { + fields: [ + { + id: 1, + name: 'Card Number', + required: true, + }, + { + id: 2, + name: 'Expiry Date', + required: true, + }, + { + id: 3, + name: 'CVV', + required: true, + }, + ], + system_name: 'Hyperwallet', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Payoneer Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'Xoom', + }, + { + fields: [ + { + id: 1, + name: 'Phone Number', + required: true, + }, + ], + system_name: 'Bradesco Transfer', + }, + { + fields: [ + { + id: 1, + name: 'Email', + required: true, + }, + ], + system_name: 'Interac e-Transfer', + }, + ]; + + const [paymentMethods, setPaymentMethods] = useState(defaultPaymentMethods); + + const [selectedPaymentMethods, setSelectedPaymentMethods] = useState([]); + + const [merchantFee, setMerchantFee] = useState(); + const [userFee, setUserFee] = useState(); + const [sourceAccount, setSourceAccount] = useState(); + const [editMode, setEditMode] = useState(false); + const [enable, setEnable] = useState(false); + const [emailOptions, setEmailOptions] = useState([]); + const [selectedEmailData, setSelectedEmailData] = useState({}); + const [p2pConfig, setP2pConfig] = useState({}); + const searchRef = useRef(null); + const [filterMethod, setFilterMethod] = useState(); + const [filterFiat, setFilterFiat] = useState(); + const [methodEditMode, setMethodEditMode] = useState(false); + useEffect(() => { + getTiers(); + }, []); + const getTiers = () => { + requestTiers() + .then((res) => { + setTiers(res); + }) + .catch((err) => { + console.error(err); + }); + }; + + useEffect(() => { + setEnable(p2p_config?.enable); + setSide(p2p_config?.side); + setDigitalCurrencies(p2p_config?.digital_currencies || []); + setFiatCurrencies(p2p_config?.fiat_currencies || []); + setMerchantTier(p2p_config?.starting_merchant_tier); + setUserTier(p2p_config?.starting_user_tier); + setSelectedPaymentMethods(p2p_config?.bank_payment_methods || []); + setMerchantFee(p2p_config?.merchant_fee); + setUserFee(p2p_config?.user_fee); + setSourceAccount(p2p_config?.source_account); + if (p2p_config?.source_account) { + getAllUserData({ id: p2p_config?.source_account }).then((res) => { + let emailData = {}; + res && + res.forEach((item) => { + if (item.value === p2p_config?.source_account) { + emailData = item; + } + }); + setSelectedEmailData(emailData); + setSourceAccount(Number(p2p_config?.source_account)); + }); + } + setP2pConfig(p2p_config); + + let methods = []; + paymentMethods.forEach((method) => { + if ( + !p2p_config?.bank_payment_methods?.find( + (x) => x.system_name == method.system_name + ) + ) { + methods.push(method); + } + }); + + setPaymentMethods([...p2p_config?.bank_payment_methods, ...methods]); + }, []); + + const handleEmailChange = (value) => { + let emailId = parseInt(value); + let emailData = {}; + emailOptions && + emailOptions.forEach((item) => { + if (item.value === emailId) { + emailData = item; + } + }); + + setSelectedEmailData(emailData); + setSourceAccount(Number(emailId)); + + handleSearch(emailData.label); + }; + + const searchUser = (searchText, type) => { + getAllUserData({ search: searchText }, type); + }; + + const handleSearch = _debounce(searchUser, 1000); + + const getAllUserData = async (params = {}) => { + try { + const res = await requestUsers(params); + if (res && res.data) { + const userData = res.data.map((user) => ({ + label: user.email, + value: user.id, + })); + setEmailOptions(userData); + + return userData; + } + } catch (error) { + return error; + } + }; + + const handleEditInput = () => { + if (searchRef && searchRef.current && searchRef.current.focus) { + searchRef.current.focus(); + } + }; + + return ( +
+
+
+ Below is the status of the P2P system on your platform and the + settings. Select what assets, KYC requirements and more are allowed on + your platform. +
+
+ + {p2pConfig?.enable !== null && ( +
+ Enable{' '} + { + try { + await updateConstants({ + kit: { + features: { + ...features, + p2p: e, + }, + p2p_config: { + enable: e, + bank_payment_methods: selectedPaymentMethods, + starting_merchant_tier: merchantTier, + starting_user_tier: userTier, + digital_currencies: digitalCurrencies, + fiat_currencies: fiatCurrencies, + side: side, + merchant_fee: merchantFee, + user_fee: userFee, + source_account: sourceAccount, + }, + }, + }); + requestAdminData().then((res) => { + const result = res?.data?.kit?.p2p_config; + setEnable(result?.enable); + setSide(result?.side); + setDigitalCurrencies(result?.digital_currencies); + setFiatCurrencies(result?.fiat_currencies); + setMerchantTier(result?.starting_merchant_tier); + setUserTier(result?.starting_user_tier); + setSelectedPaymentMethods(result?.bank_payment_methods); + setMerchantFee(result?.merchant_fee); + setUserFee(result?.user_fee); + setSourceAccount(result?.source_account); + setP2pConfig(result); + }); + } catch (error) { + message.error(error.data.message); + } + }} + /> +
+ )} +
+
+ {!p2p_config?.enable && ( +
+
+ Currently the P2P markets have not been setup on your exchange. +
+
{ + setDisplayP2pModel(true); + }} + > + → Click here to set up P2P trading +
+
+ )} +
+
+
+ Sides +
+
+ Trade sides allowed: {p2pConfig?.side}{' '} +
+
+
+ +
+
+ Crypto: +
+
+ Cryptocurrencies allowed for trading:{' '} + {p2pConfig?.digital_currencies + ?.filter((x) => x === 'usdt') + ?.join(', ')} +
+
+
+ +
+
+ Fiat: +
+
+ Fiat currencies allowed for trading:{' '} + {p2pConfig?.fiat_currencies?.join(', ')} +
+
+
+ +
+
+ Payment methods: +
+
+ Outside payment methods allowed:{' '} + {p2pConfig?.bank_payment_methods + ?.map((x) => x.system_name) + ?.join(', ')} +
+
+
+ +
+
+ Manage: +
+
+ Merchant fee: {p2pConfig?.merchant_fee}% +
+
+ Buyer fee: {p2pConfig?.user_fee}% +
+
+
+ +
+
+ Fee Source Account: +
+
+ {selectedEmailData?.label || '-'} +
+
+
+
+ + {displayP2pModel && ( + } + bodyStyle={{ + backgroundColor: '#27339D', + marginTop: 60, + }} + visible={displayP2pModel} + width={600} + footer={null} + onCancel={() => { + setEditMode(false); + setDisplayP2pModel(false); + }} + > +

P2P setup

+ + {step === 0 && ( +
+
Trade direction (side)
+
+ Select what kind of deals that the vendors (market makers) can + advertise. +
+ + + +
+ Vendors (makers) can only offer to sell crypto to users (takers) +
+
+
Crypto assets
+
Select the crypto assets that vendors can transact with
+ {Object.values(coins || {}) + .filter((coin) => coin.symbol === 'usdt') + .map((coin) => { + return ( +
+ { + if (e.target.checked) { + if (!digitalCurrencies.includes(coin.symbol)) { + setDigitalCurrencies([ + ...digitalCurrencies, + coin.symbol, + ]); + } + } else { + if (digitalCurrencies.includes(coin.symbol)) { + setDigitalCurrencies( + [...digitalCurrencies].filter( + (symbol) => symbol !== coin.symbol + ) + ); + } + } + }} + > + {coin.fullname} ({coin?.symbol?.toUpperCase()}) + +
+ ); + })} +
+ )} + + {step === 1 && ( +
+
Fiat currencies
+
+ Select the fiat currencies to be used for P2P trading on your + platform. +
+ +
{ + setDisplayFiatAdd(true); + }} + > + Add a fiat currency +
+ +
+ {fiatCurrencies.length === 0 ? ( + <> +
No fiat asset added yet.
+
{ + setDisplayFiatAdd(true); + }} + > + Add here +
+ + ) : ( +
+ {fiatCurrencies.map((symbol) => { + return ( +
+ + {' '} + + {' '} + {symbol?.toUpperCase()} + + +
+ ); + })} +
+ )} +
+
+ )} + + {step === 2 && ( +
+

Requirements

+

+ Set the minimum account tier required for users and vendor to + use and create P2P trades. +

+

+ Users are the end users accessing the P2P trade and Vendors are + the users who create P2P deals. Vendors (market makers) can + setup public P2P deals with their own prices however should be + held to a higher standard as stipulated from your account tier + levels. +

+ +
+
User account minimum tier
+
+ + {' '} + Minimum tier for the user's account to participate in P2P + trading + {' '} + (it is recommended to select a tier level that requires KYC + verification) +
+ +
+ +
Vendor account minimum tier
+
+ + {' '} + Minimum tier account level to be a Vender + {' '} + (higher or equal than user account) +
+ +
+ )} + + {step === 3 && ( +
+
Payment methods
+
+ Select the fiat payment methods that will be used to settle + transactions between P2P buyers and sellers. These methods will + be used outside of your platform and should ideally include the + fiat currencies that you have enabled for P2P trading on your + platform. +
+ +
setDisplayPaymentAdd(true)} + > + Add/create a method +
+ +
+ {selectedPaymentMethods.length === 0 ? ( + <> +
No payment accounts added yet.
+
{ + setDisplayPaymentAdd(true); + }} + > + Add here +
+ + ) : ( +
+ {selectedPaymentMethods.map((x, index) => { + return ( +
+ {x.system_name} + { + setDisplayNewPayment(true); + setMethodEditMode(true); + setCustomFields(x.fields); + setPaymentMethod({ + ...paymentMethod, + system_name: x.system_name, + selected_index: index, + }); + }} + > + EDIT + + { + setSelectedPaymentMethods( + [...selectedPaymentMethods].filter( + (a) => a.system_name !== x.system_name + ) + ); + }} + > + X + +
+ ); + })} +
+ )} +
+ {/*
+
+
ICON
+
🆕 PayPal (Custom)
+
EDIT
+
REMOVE
+
+
*/} +
+ )} + + {step === 4 && ( +
+
Platform management fees
+
+ Apply a trading fee upon every P2P transaction that uses your + platform. The fee percentage will be split between both vendor + (maker) and user (taker) evenly. +
+ +
+
+ P2P Vendor percent trade fee +
+ { + setMerchantFee(e.target.value); + }} + /> +
+ +
+
+ P2P Buyer percent trade fee +
+ { + setUserFee(e.target.value); + }} + /> +
+ +
+
Account to send the fees to
+
+ +
+ Edit +
+
+
+ +
+
Vendor fee: {merchantFee || 0}%
+
User fee:{userFee || 0}%
+
+ +
+ The minimum fee allowed to apply is dependent on exchange + system's plan: +
+
https://www.hollaex.com/pricing
+
+ )} + + {step === 5 && ( +
+
Review and confirm
+
Below are your P2P settings
+ +
+ Please check the details below are correct and confirm.{' '} +
+
+ After confirming the P2P market page will be in view and + available for user merchants and traders +
+ +
+
+
+
Type of P2P deals:
+
{side?.toUpperCase()}
+
+
{ + setStep(0); + }} + style={{ cursor: 'pointer' }} + > + EDIT +
+
+
+ +
+
+
+
Cryptocurrencies allowed for trading:
+
{digitalCurrencies.join(', ')}
+
+
{ + setStep(0); + }} + style={{ cursor: 'pointer' }} + > + EDIT +
+
+
+ +
+
+
+
Fiat currencies allowed for trading:
+
{fiatCurrencies.join(', ')}
+
+
{ + setStep(0); + }} + style={{ cursor: 'pointer' }} + > + EDIT +
+
+
+ +
+
+
+
Outside payment methods allowed:
+
+ {selectedPaymentMethods + .map((x) => x.system_name) + ?.join(', ')} +
+
+
{ + setStep(0); + }} + style={{ cursor: 'pointer' }} + > + EDIT +
+
+
+ +
+
+
+
Merchant fee:
+
{merchantFee}
+
+
{ + setStep(0); + }} + style={{ cursor: 'pointer' }} + > + EDIT +
+
+
+ +
+
+
+
Buyer fee:
+
{userFee}
+
+
{ + setStep(0); + }} + style={{ cursor: 'pointer' }} + > + EDIT +
+
+
+ +
+
+
+
Fee Source Account:
+
{selectedEmailData.label}
+
+
{ + setStep(0); + }} + style={{ cursor: 'pointer' }} + > + EDIT +
+
+
+ + {/*
+
+
+
Type of P2P deals:
+
{side?.toUpperCase()}
+
+
EDIT
+
+
*/} +
+ )} + +
+ + +
+
+ )} + + {displayFiatAdd && ( + } + bodyStyle={{ + backgroundColor: '#27339D', + marginTop: 60, + }} + visible={displayP2pModel} + width={450} + footer={null} + onCancel={() => { + setDisplayFiatAdd(false); + }} + > +

+ Select fiat to add +

+ +
+
Search fiat
+ + { + setFilterFiat(e.target.value); + }} + /> + +
+ Fiat: +
+
+ {Object.values(coins || {}) + .filter((coin) => coin.type === 'fiat') + .filter((x) => + filterFiat?.length > 0 + ? x.symbol.toLowerCase().includes(filterFiat?.toLowerCase()) + : true + ) + .map((coin) => { + return ( +
{ + if (!fiatCurrencies.includes(coin.symbol)) { + setFiatCurrencies([...fiatCurrencies, coin.symbol]); + } else { + setFiatCurrencies( + [...fiatCurrencies].filter( + (symbol) => symbol !== coin.symbol + ) + ); + } + }} + > + {/* ICON{' '} */} + + {coin.fullname}({coin.symbol}) + +
+ ); + })} +
+
+ +
+ +
+
+ )} + + {displayPaymentAdd && ( + } + bodyStyle={{ + backgroundColor: '#27339D', + marginTop: 60, + }} + visible={displayP2pModel} + width={450} + footer={null} + onCancel={() => { + setDisplayPaymentAdd(false); + }} + > +

+ Select payment methods to add +

+ +
+
Search payment methods
+ {/* */} + + { + setFilterMethod(e.target.value); + }} + /> + +
+ Payment Methods: +
+
+ {paymentMethods.length === 0 ? ( +
No payment methods selected
+ ) : ( +
+ {/* ICON */} + {paymentMethods + .filter((x) => + filterMethod?.length > 0 + ? x.system_name + .toLowerCase() + .includes(filterMethod?.toLowerCase()) + : true + ) + .map((method) => { + return ( +
x.system_name === method.system_name + ) + ? 'bold' + : '200', + }} + onClick={() => { + if ( + !selectedPaymentMethods.find( + (x) => x.system_name === method.system_name + ) + ) { + setSelectedPaymentMethods([ + ...selectedPaymentMethods, + method, + ]); + } else { + setSelectedPaymentMethods( + [...selectedPaymentMethods].filter( + (x) => x.system_name !== method.system_name + ) + ); + } + }} + > + {method?.system_name} +
+ ); + })} +
+ )} +
+
+ +
+ +
+
+ Can't find your local payment method? +
+
{ + setDisplayNewPayment(true); + }} + > + Create and add a new payment method +
+
+ )} + + {displayNewPayment && ( + } + bodyStyle={{ + backgroundColor: '#27339D', + marginTop: 60, + }} + visible={displayP2pModel} + width={600} + footer={null} + onCancel={() => { + setDisplayNewPayment(false); + }} + > +

+ {methodEditMode + ? 'Edit payment methods' + : 'Create and add new payment methods'} +

+
+ To add a payment method to your P2P platform, you can do so manually + by entering the name of the payment method and the required payment + details. For example, PayPal uses email addresses to send and + receive funds.{' '} +
+ +
+ Once the payment method is added, your P2P merchants and users will + be able to select it and enter the necessary information when making + or receiving payments. The details they provide will be shared with + the other party in the P2P transaction. +
+ +
+ Name of method and main payment detail +
+ +
+
Create new payment methods
+ { + setPaymentMethod({ + ...paymentMethod, + system_name: e.target.value, + }); + }} + /> +
+ + {customFields.map((field) => { + return ( +
+
+ FIELD {field.id}# +
+
Payment detail name
+ { + const newCustomFields = [...customFields]; + const found = newCustomFields.find( + (x) => x.id === field.id + ); + if (found) { + found.name = e?.target?.value; + } + + setCustomFields(newCustomFields); + }} + /> +
+ ); + })} + +
{ + setPaymentFieldAdd(true); + }} + > + Add new payment detail field +
+
+ + + +
+
+ )} + + {paymentFieldAdd && ( + } + bodyStyle={{ + backgroundColor: '#27339D', + marginTop: 60, + }} + visible={displayP2pModel} + width={450} + footer={null} + onCancel={() => { + setPaymentFieldAdd(false); + }} + > +

+ Add an additional payment details +

+
+ This new payment field is additional and should assist P2P + participants in their fiat currency transfers. This should be + account details related to payment method. This may including phone + numbers, usernames, unique account numbers, and other necessary + information for transactions depending on the payment methods + system.{' '} +
+ +
+
Payment detail name
+ { + setCustomField({ + ...customField, + name: e.target.value, + }); + }} + /> +
+ +
+
Required or optional
+
+ + + + Required + +
+ (Important payment detail) +
+ + Optional + +
+ (Optional payment detail) +
+
+
+
+
+ +
+ + + +
+
+ )} +
+ ); +}; + +const mapStateToProps = (state) => ({ + exchange: state.asset && state.asset.exchange, + pairs: state.app.pairs, + coins: state.app.coins, + user: state.user, + quicktrade: state.app.allContracts.quicktrade, + networkQuickTrades: state.app.allContracts.networkQuickTrades, + coinObjects: state.app.allContracts.coins, + broker: state.app.broker, + features: state.app.constants.features, + p2p_config: state.app.constants.p2p_config, +}); + +const mapDispatchToProps = (dispatch) => ({ + setExchange: bindActionCreators(setExchange, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(P2PSettings); diff --git a/web/src/containers/Admin/User/Referrals.js b/web/src/containers/Admin/User/Referrals.js index 5e7f977535..8eba21030f 100644 --- a/web/src/containers/Admin/User/Referrals.js +++ b/web/src/containers/Admin/User/Referrals.js @@ -1,10 +1,16 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { Table, Spin, Alert } from 'antd'; -import { getUserReferer, getUserAffiliation } from './actions'; +import { Table, Spin, Alert, message, Select, Modal, Button } from 'antd'; +import { + getUserReferer, + getUserAffiliation, + fetchReferralCodesByAdmin, + postReferralCodeByAdmin, +} from './actions'; import { formatTimestampGregorian, DATETIME_FORMAT } from 'utils/date'; +import { CloseOutlined } from '@ant-design/icons'; import './index.css'; -const COLUMNS = [ +const AFF_COLUMNS = [ { title: 'Time referred /signed up', dataIndex: 'created_at', @@ -28,9 +34,76 @@ const COLUMNS = [ }, ]; +const REF_COLUMNS = [ + { + title: 'Creation Date', + dataIndex: 'created_at', + key: 'created_at', + render: (value) => ( +
+ {formatTimestampGregorian(value, DATETIME_FORMAT)} +
+ ), + }, + { + title: 'Code', + dataIndex: 'code', + key: 'code', + render: (data, key, index) => ( +
{data || '-'}
+ ), + }, + { + title: 'Referral Count', + dataIndex: 'referral_count', + key: 'referral_count', + + render: (data, key, index) => { + return
{data}
; + }, + }, + { + title: 'Earning rate', + dataIndex: 'earning_rate', + key: 'earning_rate', + + render: (data, key, index) => { + return
{data}%
; + }, + }, + { + title: 'Discount given', + dataIndex: 'discount', + key: 'discount', + + render: (data, key, index) => { + return
{data}%
; + }, + }, + { + title: 'Link', + label: 'link', + key: 'link', + className: 'd-flex justify-content-end', + render: (data, key, index) => { + return ( +
+ .../signup?affiliation_code={data?.code}{' '} +
+ ); + }, + }, +]; + const LIMIT = 50; -const Referrals = ({ userInformation: { id: userId, affiliation_code } }) => { +const Referrals = ({ + userInformation: { id: userId, affiliation_code }, + referral_history_config, +}) => { const [loading, setLoading] = useState(true); const [invitedBy, setInvitedBy] = useState(); const [data, setData] = useState([]); @@ -39,11 +112,18 @@ const Referrals = ({ userInformation: { id: userId, affiliation_code } }) => { const [count, setCount] = useState(); const [isRemaining, setIsRemaining] = useState(true); const [error, setError] = useState(); - const referralLink = `${process.env.REACT_APP_PUBLIC_URL}/signup?affiliation_code=${affiliation_code}`; + const [displayCreateReferralCode, setDisplayCreateReferralCode] = useState( + false + ); + const [referralPayload, setReferralPayload] = useState({}); + const [referralCode, setReferralCode] = useState(); const requestAffiliations = useCallback( (page, limit) => { - getUserAffiliation(userId, page, limit) + let action = referral_history_config?.active + ? fetchReferralCodesByAdmin + : getUserAffiliation; + action(userId, page, limit) .then((response) => { setData((prevData) => page === 1 ? response.data : [...prevData, ...response.data] @@ -62,7 +142,7 @@ const Referrals = ({ userInformation: { id: userId, affiliation_code } }) => { setError(message); }); }, - [userId] + [userId, referral_history_config.active] ); const onPageChange = (count, pageSize) => { @@ -92,8 +172,167 @@ const Referrals = ({ userInformation: { id: userId, affiliation_code } }) => { ); } + const generateUniqueCode = () => { + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let code = ''; + + for (let i = 0; i < 6; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + code += characters[randomIndex]; + } + + return code; + }; return (
+ {displayCreateReferralCode && ( + } + bodyStyle={{ + backgroundColor: '#27339D', + marginTop: 60, + }} + visible={displayCreateReferralCode} + footer={null} + onCancel={() => { + setDisplayCreateReferralCode(false); + }} + > +

+ Create Referral Code +

+
+ You can create referral code for the selected user below +
+
+
+
Code
+ { + setReferralCode(e.target.value); + }} + /> +
+ +
+
Earning Rate
+ +
+ +
+
Discount
+ +
+
+ +
+ + +
+
+ )}
Referral affiliation information and table displaying all the successful referrals that were onboarded onto the platform from this user. @@ -105,14 +344,42 @@ const Referrals = ({ userInformation: { id: userId, affiliation_code } }) => {
-
Total referred:
+
+ {referral_history_config?.active + ? 'Number of generated codes:' + : 'Total referred:'} +
{count}
-
-
+ {/*
*/} + {/*
Referral link:
{referralLink}
-
+
*/} + {referral_history_config?.active && ( +
+ +
+ )}
{error && ( @@ -125,10 +392,9 @@ const Referrals = ({ userInformation: { id: userId, affiliation_code } }) => { closeText="Close" /> )} -
Affiliation referral table
- + } { diff --git a/web/src/containers/Admin/User/actions.js b/web/src/containers/Admin/User/actions.js index 07ef945f0a..73788c270c 100644 --- a/web/src/containers/Admin/User/actions.js +++ b/web/src/containers/Admin/User/actions.js @@ -275,6 +275,24 @@ export const getUserAffiliation = (user_id, page = 1, limit = 50) => { return requestAuthenticated(`/admin/user/affiliation?${query}`, options); }; +export const fetchReferralCodesByAdmin = (user_id, page = 1, limit = 50) => { + const params = { user_id, page, limit }; + const query = querystring.stringify(params); + + const options = { + method: 'GET', + }; + return requestAuthenticated(`/admin/user/referral/code?${query}`, options); +}; + +export const postReferralCodeByAdmin = (values) => { + const options = { + method: 'POST', + body: JSON.stringify(values), + }; + return requestAuthenticated(`/admin/user/referral/code`, options); +}; + export const getUserReferer = (user_id) => { const options = { method: 'GET', diff --git a/web/src/containers/Admin/User/index.js b/web/src/containers/Admin/User/index.js index 26de751330..d04582baae 100644 --- a/web/src/containers/Admin/User/index.js +++ b/web/src/containers/Admin/User/index.js @@ -240,6 +240,7 @@ class App extends Component { refreshData={this.refreshData} onChangeUserDataSuccess={this.onChangeUserDataSuccess} requestUserData={this.requestUserData} + referral_history_config={this.props.referral_history_config} /> ) : (
@@ -256,6 +257,7 @@ class App extends Component { const mapStateToProps = (state) => ({ pluginNames: state.app.pluginNames, coins: state.app.coins, + referral_history_config: state.app.constants.referral_history_config, constants: state.app.constants, }); diff --git a/web/src/containers/App/App.js b/web/src/containers/App/App.js index 68f943362d..9ffc3f146c 100644 --- a/web/src/containers/App/App.js +++ b/web/src/containers/App/App.js @@ -31,8 +31,7 @@ import { storeTools } from 'actions/toolsAction'; import STRINGS from 'config/localizedStrings'; import { getChatMinimized, setChatMinimized } from 'utils/theme'; -import { checkUserSessionExpired } from 'utils/utils'; -import { getTokenTimestamp, isLoggedIn, isAdmin } from 'utils/token'; +import { isLoggedIn, isAdmin } from 'utils/token'; import { AppBar, AppMenuBar, @@ -100,9 +99,6 @@ class App extends Component { this.setState({ chatIsClosed, }); - if (isLoggedIn() && checkUserSessionExpired(getTokenTimestamp())) { - this.logout('Token is expired'); - } } componentDidMount() { diff --git a/web/src/containers/Deposit/Deposit.js b/web/src/containers/Deposit/Deposit.js new file mode 100644 index 0000000000..e31278b20c --- /dev/null +++ b/web/src/containers/Deposit/Deposit.js @@ -0,0 +1,724 @@ +import React, { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { isMobile } from 'react-device-detect'; +import { Input, Modal, Select, Button } from 'antd'; +import { + CaretDownOutlined, + CheckOutlined, + CloseOutlined, + ExclamationCircleFilled, +} from '@ant-design/icons'; + +import { + depositCurrency, + depositNetwork, + depositNetworkOptions, +} from 'actions/appActions'; +import { Coin, EditWrapper } from 'components'; +import { STATIC_ICONS } from 'config/icons'; +import { assetsSelector } from 'containers/Wallet/utils'; +import { renderLabel, renderNetworkWithLabel } from 'containers/Withdraw/utils'; +import STRINGS from 'config/localizedStrings'; +import { onHandleSymbol } from './utils'; + +const DepositComponent = ({ + coins, + pinnedAssets, + assets, + currency, + openQRCode, + onOpen, + onCopy, + updateAddress, + depositAddress, + router, + selectedNetwork, + ...rest +}) => { + const { Option } = Select; + const { + setDepositNetworkOptions, + setDepositNetwork, + setDepositCurrency, + getDepositNetworkOptions, + getDepositCurrency, + } = rest; + + const [currStep, setCurrStep] = useState({ + stepOne: false, + stepTwo: false, + stepThree: false, + stepFour: false, + }); + const [selectedAsset, setSelectedAsset] = useState(null); + const [topAssets, setTopAssets] = useState([]); + const [isPinnedAssets, setIsPinnedAssets] = useState(false); + const [optionalTag, setOptionalTag] = useState(''); + const [isDisbaleDeposit, setIsDisbaleDeposit] = useState(false); + const [isVisible, setIsVisible] = useState(false); + + const defaultCurrency = currency !== '' && currency; + const address = depositAddress?.split(':'); + const isTag = address && address[1]; + + const coinLength = + coins[getDepositCurrency]?.network && + coins[getDepositCurrency]?.network.split(','); + let network = + coins[getDepositCurrency]?.network && + coins[getDepositCurrency]?.network !== 'other' + ? coins[getDepositCurrency]?.network + : coins[getDepositCurrency]?.symbol; + const defaultNetwork = + defaultCurrency && + coins[defaultCurrency]?.network && + coins[defaultCurrency]?.network !== 'other' + ? coins[defaultCurrency]?.network + : coins[defaultCurrency]?.symbol; + const isSteps = + (coinLength && coinLength.length === 1 && !isDisbaleDeposit) || + (currStep.stepTwo && !coinLength) || + currStep.stepThree; + const iconId = coins[getDepositCurrency]?.icon_id || coins[currency]?.icon_id; + const currentCurrency = getDepositCurrency ? getDepositCurrency : currency; + const min = coins[currentCurrency]; + const isDeposit = coins[getDepositCurrency]?.allow_deposit; + + useEffect(() => { + const topWallet = assets + .filter((item, index) => { + return index <= 3; + }) + .map((data) => { + return data[0]; + }); + if (pinnedAssets.length) { + setTopAssets(pinnedAssets); + } else { + setTopAssets(topWallet); + } + if (defaultCurrency) { + if (['xrp', 'xlm', 'ton'].includes(defaultCurrency)) { + setCurrStep({ + ...currStep, + stepTwo: true, + stepThree: true, + stepFour: true, + }); + } else { + setCurrStep({ ...currStep, stepTwo: true }); + } + if ( + ['xrp', 'xlm', 'ton', 'pmn'].includes(defaultCurrency) || + ['xrp', 'xlm', 'ton', 'pmn'].includes(defaultNetwork) + ) { + setIsVisible(true); + } + setDepositCurrency(defaultCurrency); + setSelectedAsset(defaultCurrency); + updateAddress(defaultCurrency); + setDepositNetwork(defaultNetwork); + } + return () => { + setDepositNetworkOptions(null); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (isTag) { + setOptionalTag(address[1]); + } else { + setOptionalTag(''); + } + }, [address, isTag]); + + useEffect(() => { + if (getDepositCurrency && !isDeposit) { + setSelectedAsset(''); + setIsDisbaleDeposit(true); + setCurrStep({ + stepOne: true, + stepTwo: false, + stepThree: false, + stepFour: false, + }); + } else { + setIsDisbaleDeposit(false); + } + }, [getDepositCurrency, isDeposit]); + + const onHandleChangeSelect = (val, pinned_assets = false) => { + if (pinned_assets) { + setIsPinnedAssets(pinned_assets); + } + if (val) { + if (currStep.stepTwo || currStep.stepThree || currStep.stepFour) { + if (['xrp', 'xlm', 'ton'].includes(val)) { + setCurrStep((prev) => ({ + ...prev, + stepTwo: true, + stepThree: true, + stepFour: true, + })); + } else { + setCurrStep((prev) => ({ + ...prev, + stepTwo: true, + stepThree: false, + stepFour: false, + })); + } + } + if (coins[val] && !coins[val].allow_deposit) { + setCurrStep((prev) => ({ ...prev, stepTwo: false })); + } else { + setCurrStep((prev) => ({ ...prev, stepTwo: true })); + } + setDepositCurrency(val); + network = val ? val : coins[getDepositCurrency]?.symbol; + setDepositNetworkOptions(null); + } else if (!val) { + setDepositCurrency(''); + setCurrStep((prev) => ({ + ...prev, + stepTwo: false, + stepThree: false, + stepFour: false, + })); + } + let currentNetwork = + coins[val]?.network && coins[val]?.network !== 'other' + ? coins[val]?.network + : coins[val]?.symbol; + setDepositNetwork(currentNetwork); + setSelectedAsset(val && coins[val] && coins[val].allow_deposit ? val : ''); + updateAddress(val); + router.push(`/wallet/${val}/deposit`); + }; + + const renderPinnedAsset = (data) => { + const icon_id = coins[data]?.icon_id; + return ( +
+ {data.toUpperCase()} + + + +
+ ); + }; + + const onHandleChangeNetwork = (val) => { + if (val) { + setCurrStep((prev) => ({ ...prev, stepThree: true })); + setDepositNetworkOptions(val); + updateAddress(val, true); + setDepositNetwork(val); + } else if (!val) { + setCurrStep((prev) => ({ ...prev, stepThree: false, stepFour: false })); + } + }; + + const renderScanIcon = (isTag = false) => { + return ( +
+ {!isTag && ( + <> +
openQRCode()} + > +
+ scan-icon +
+
+
+ + )} + +
onCopy()}> + {renderLabel('COPY_TEXT')} +
+
+
+ ); + }; + + const onHandleClear = (type) => { + if (type === 'coin') { + setSelectedAsset(null); + setDepositCurrency(''); + setCurrStep({ + ...currStep, + stepTwo: false, + stepThree: false, + stepFour: false, + }); + } + if (type === 'network') { + setDepositNetworkOptions(null); + } + }; + + const onHandleSelect = (symbol) => { + const curr = onHandleSymbol(symbol); + if (curr !== symbol) { + if ( + ['xrp', 'xlm', 'ton'].includes(defaultCurrency) || + ['xrp', 'xlm', 'ton'].includes(defaultNetwork) + ) { + setIsVisible(true); + } else { + setIsVisible(false); + } + } + }; + + const renderDepositWarningPopup = () => { + return ( +
+
+ + {STRINGS['WITHDRAW_PAGE.WARNING_DEPOSIT_INFO_1']} + +
+
+ + {STRINGS['WITHDRAW_PAGE.WARNING_DEPOSIT_INFO_2']} + +
+
+ +
+
+ ); + }; + + const renderOptionalField = + (['xrp', 'xlm'].includes(selectedAsset) || + ['xlm', 'ton'].includes(network)) && + depositAddress; + const networkIcon = selectedNetwork + ? coins[selectedNetwork]?.icon_id + : coins[defaultNetwork]?.icon_id; + const networkOptionsIcon = coins[getDepositNetworkOptions]?.icon_id; + + return ( +
+
+
+
+ 1 + +
+
+
+ {renderLabel('ACCORDIAN.SELECT_ASSET')} +
+
+
+ {topAssets.map((data, inx) => { + return ( + onHandleChangeSelect(data, true)} + > + {renderPinnedAsset(data)} + + ); + })} +
+
+ + {currStep.stepTwo && } +
+
+
+
+
+
+
+
+ + 2 + + +
+
+
+ {renderLabel('ACCORDIAN.SELECT_NETWORK')} +
+ {currStep.stepTwo && ( +
1 ? '' : 'deposit-network-field' + }` + } + > +
+ + {(coinLength && + coinLength.length === 1 && + !isDisbaleDeposit) || + (currStep.stepTwo && !coinLength) || + currStep.stepThree ? ( + + ) : ( + + )} +
+
+ +
+ {renderLabel('DEPOSIT_FORM_NETWORK_WARNING')} +
+
+
+ )} +
+
+
+
+
+
+ + 3 + + {renderOptionalField && ( + + )} +
+
+
+
+ {getDepositCurrency && ( + + + + )} + + {getDepositCurrency.toUpperCase()} + +
+
+ {renderLabel('SUMMARY.DEPOSIT')} +
+ + {renderLabel('ACCORDIAN.ADDRESS')} + +
+ {((coinLength && coinLength.length === 1 && !isDisbaleDeposit) || + (currStep.stepTwo && !coinLength) || + currStep.stepThree) && + (!depositAddress && selectedAsset ? ( +
+
+
+ {renderLabel('WITHDRAW_PAGE.GENERATE_DEPOSIT_TEXT_1')} +
+
+ {renderLabel('WITHDRAW_PAGE.GENERATE_DEPOSIT_TEXT_2')} +
+
+
+ +
+
+ ) : ( +
+
+ +
+
+ +
+ + {STRINGS.formatString( + STRINGS['DEPOSIT_FORM_MIN_WARNING'], + min?.min, + currentCurrency.toUpperCase() + )} + +
+
+
+ ))} +
+
+
+ {renderOptionalField && ( +
+
+
+ + 4 + +
+
+
+
+ + + + {renderLabel('ACCORDIAN.TAG')} +
+ {((coinLength && coinLength.length === 1) || + (currStep.stepTwo && !coinLength) || + currStep.stepThree) && ( +
+
+ +
+
+ +
+ {renderLabel( + 'DEPOSIT_FORM_TITLE_WARNING_DESTINATION_TAG' + )} +
+
+
+ )} +
+
+
+ setIsVisible(false)} + footer={false} + className="withdrawal-remove-tag-modal" + width={'420px'} + > + {renderDepositWarningPopup()} + +
+ )} +
+ ); +}; + +const mapStateToProps = (state) => ({ + getDepositCurrency: state.app.depositFields.depositCurrency, + getDepositNetworkOptions: state.app.depositFields.depositNetworkOptions, + pinnedAssets: state.app.pinned_assets, + assets: assetsSelector(state), +}); + +const mapDispatchToProps = (dispatch) => ({ + setDepositCurrency: bindActionCreators(depositCurrency, dispatch), + setDepositNetwork: bindActionCreators(depositNetwork, dispatch), + setDepositNetworkOptions: bindActionCreators(depositNetworkOptions, dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(DepositComponent); diff --git a/web/src/containers/Deposit/Fiat/DepositForm.js b/web/src/containers/Deposit/Fiat/DepositForm.js index 5ef7d2bcd0..e4a4b7f50a 100644 --- a/web/src/containers/Deposit/Fiat/DepositForm.js +++ b/web/src/containers/Deposit/Fiat/DepositForm.js @@ -185,11 +185,11 @@ export const generateInitialValues = ( verification_level, coins, currency, - fiat_fees + fiat_fees, + getDepositCurrency ) => { const initialValues = {}; - - let { rate } = getFiatDepositFee(currency); + let { rate } = getFiatDepositFee(currency, 0, '', getDepositCurrency); rate = fiat_fees?.[currency]?.deposit_fee || rate; initialValues.fee = rate; return initialValues; @@ -369,6 +369,7 @@ const mapStateToProps = (state, ownProps) => { user: state.user, onramp: depositOptionsSelector(state, ownProps), fiat_fees: state.app.constants.fiat_fees, + getDepositCurrency: state.app.depositFields.depositCurrency, }; }; diff --git a/web/src/containers/Deposit/Fiat/Form.js b/web/src/containers/Deposit/Fiat/Form.js index 12cbab8a56..5ea43a7e08 100644 --- a/web/src/containers/Deposit/Fiat/Form.js +++ b/web/src/containers/Deposit/Fiat/Form.js @@ -44,16 +44,24 @@ const Form = ({ coins, onramp = {}, fiat_fees, + getDepositCurrency, }) => { const [activeTab, setActiveTab] = useState(); const [tabs, setTabs] = useState({}); const [activeStep, setActiveStep] = useState(STEPS.HOME); const [initialValues, setInitialValues] = useState({}); + const currentCurrency = getDepositCurrency ? getDepositCurrency : currency; + useEffect(() => { setTabs(getTabs()); setInitialValues( - generateInitialValues(verification_level, coins, currency, fiat_fees) + generateInitialValues( + verification_level, + coins, + currentCurrency, + fiat_fees + ) ); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -294,7 +302,9 @@ const Form = ({ ); }; - const { icon_id } = coins[currency] || DEFAULT_COIN_DATA; + const { icon_id } = getDepositCurrency + ? coins[getDepositCurrency] + : coins[currency] || DEFAULT_COIN_DATA; return (
@@ -322,6 +332,7 @@ const mapStateToProps = (store, ownProps) => ({ coins: store.app.coins, onramp: store.app.onramp[ownProps.currency], fiat_fees: store.app.constants.fiat_fees, + getDepositCurrency: store.app.depositFields.depositCurrency, }); export default connect(mapStateToProps)(withRouter(withConfig(Form))); diff --git a/web/src/containers/Deposit/Fiat/utils.js b/web/src/containers/Deposit/Fiat/utils.js index 86c6adb361..85dfcbbdfc 100644 --- a/web/src/containers/Deposit/Fiat/utils.js +++ b/web/src/containers/Deposit/Fiat/utils.js @@ -6,13 +6,15 @@ const DEPOSIT_FEES_KEY = 'deposit_fees'; const WITHDRAWAL_LIMIT_KEY = 'withdrawal_limit'; const DEPOSIT_LIMIT_KEY = 'deposit_limit'; -export const getFiatFee = (currency, amount, network, type) => { +export const getFiatFee = (currency, amount, network, type, getCurrency) => { const { app: { coins }, } = store.getState(); const feeNetwork = network || currency; - const { [type]: fees, [type.slice(0, -1)]: fee } = coins[currency]; + const { [type]: fees, [type.slice(0, -1)]: fee } = getCurrency + ? coins[getCurrency] + : coins[currency]; if (fees && fees[feeNetwork]) { const { symbol, value } = fees[feeNetwork]; @@ -32,11 +34,27 @@ export const getFiatFee = (currency, amount, network, type) => { return { symbol: currency, rate: 0, value: 0 }; }; -export const getFiatWithdrawalFee = (currency, amount = 0, network) => - getFiatFee(currency, amount, network, WITHDRAWAL_FEES_KEY); +export const getFiatWithdrawalFee = ( + currency, + amount = 0, + network, + getWithdrawCurrency +) => + getFiatFee( + currency, + amount, + network, + WITHDRAWAL_FEES_KEY, + getWithdrawCurrency + ); -export const getFiatDepositFee = (currency, amount = 0, network) => - getFiatFee(currency, amount, network, DEPOSIT_FEES_KEY); +export const getFiatDepositFee = ( + currency, + amount = 0, + network, + getDepositCurrency +) => + getFiatFee(currency, amount, network, DEPOSIT_FEES_KEY, getDepositCurrency); export const getFiatLimit = (type, currency) => { const transactionType = diff --git a/web/src/containers/Deposit/QRCode.js b/web/src/containers/Deposit/QRCode.js index c05f697371..6c099fc09b 100644 --- a/web/src/containers/Deposit/QRCode.js +++ b/web/src/containers/Deposit/QRCode.js @@ -51,9 +51,19 @@ const QrCode = ({ closeQRCode, data = '', currency = '', onCopy }) => {
)} -
- -
+
+ + + +
); diff --git a/web/src/containers/Deposit/_Deposit.scss b/web/src/containers/Deposit/_Deposit.scss index f689f3e9f0..f8f1f19901 100644 --- a/web/src/containers/Deposit/_Deposit.scss +++ b/web/src/containers/Deposit/_Deposit.scss @@ -33,17 +33,29 @@ $qr-code--size: 12rem; $qr-code-margin: 1rem; width: $qr-code--size; max-width: $qr-code--size; + .qr-code-bg { background-color: white; margin: $qr-code-margin; + > *:first-child { @include size-important($qr-code--size - $qr-code-margin); } } } +.qr-popup-buttons { + display: flex; + justify-content: space-between; + .qr-back-btn, + .qr-copy-btn { + width: 45%; + } +} + .deposit_info-qr-wrapper { $qr-code-margin: 1rem; + .qr-text { text-align: center; display: flex; @@ -82,9 +94,11 @@ $qr-code--size: 12rem; .verification-content-wrapper { position: relative; } + .error { font-size: $font-size-subhead2; } + .block-wrapper { margin: 1rem; } @@ -95,3 +109,228 @@ $qr-code--size: 12rem; fill: $link; } } + +.deposit-wrapper-fields { + .currency-label { + border: 1px solid $colors-black; + padding: 0.5%; + border-radius: 5px; + margin-right: 10px; + width: 4rem; + text-align: center; + cursor: pointer; + } + + .step3-icon-wrapper { + height: 0%; + width: fit-content; + } + + .btn-wrapper-deposit { + margin-left: 3%; + .holla-button { + background-color: $link; + color: $colors-main-black; + width: 300px; + border: none; + } + } + + .generate-field-wrapper { + display: flex; + flex-direction: column; + width: 55%; + justify-content: space-around; + margin-right: 15%; + } + + .deposit-network-field { + .ant-select-selection-item { + cursor: not-allowed; + } + } +} + +.deposit-address-wrapper { + width: 68.5%; + + .deposit-address-field { + .destination-input-field, + .destination-input-field:focus-within { + border: none !important; + border-bottom: 1px solid $colors-main-border !important; + } + + .ant-input-affix-wrapper-focused { + box-shadow: none; + } + + .ant-input-suffix { + width: 15% !important; + } + + .divider { + border-right: 1px solid var(--calculated_secondary-border); + padding-left: 15%; + height: 16px; + margin-top: 4%; + } + + .render-deposit-scan-wrapper { + text-transform: uppercase; + color: $link; + cursor: pointer; + + div { + text-decoration: underline; + } + } + + .ant-input { + color: var(--specials_buttons-links-and-highlights) !important; + } + } + + .address-warning-text { + div { + text-wrap: wrap; + width: 95%; + justify-content: unset !important; + } + } +} + +.destination-tag-field-wrapper { + margin-left: 28%; + + .destination-tag-field { + .destination-input-field, + .destination-input-field:focus-within { + border: none !important; + border-bottom: 1px solid $colors-main-border !important; + } + + .destination-input-field { + width: 100% !important; + } + + .render-deposit-scan-wrapper { + text-transform: uppercase; + color: $link; + cursor: pointer; + + div { + text-decoration: underline; + } + } + + .ant-input-suffix { + width: auto !important; + } + + .ant-input:focus, + .ant-input-affix-wrapper-focused { + box-shadow: none; + } + + .ant-input { + color: var(--specials_buttons-links-and-highlights) !important; + } + } + + .tag-text { + div { + text-wrap: balance; + } + } +} + +.withdrawal-container + .withdraw-form-wrapper + .withdraw-form + .deposit-wrapper-fields { + .custom-field .custom-line-large { + height: 90px; + border: 1px solid $colors-main-black; + margin: 0 100%; + } + + .custom-field .custom-line-extra-large { + height: 2rem; + border: 1px solid $colors-main-black; + margin: 0 100%; + } +} + +.generate-deposit-label { + color: $colors-black; + margin: 1% 3%; +} + +.layout-mobile { + .withdrawal-container { + .withdraw-form-wrapper { + .withdraw-form { + .deposit-wrapper-fields { + .custom-field .custom-line-large { + height: 11rem; + border: 1px solid $colors-main-black; + margin: 0 100%; + } + + .generate-field-wrapper { + margin-left: unset; + } + + .deposit-address-wrapper { + width: unset; + + .destination-input-field { + width: 88% !important; + } + } + } + + .destination-tag-field-wrapper { + .destination-tag-field { + width: unset !important; + } + + .destination-input-field { + width: 80% !important; + } + } + } + } + } + + @media screen and (max-width: 550px) { + .withdrawal-container { + .withdraw-form-wrapper { + .withdraw-form { + .deposit-wrapper-fields { + .custom-field .custom-line-large { + height: 17rem; + } + } + } + } + } + } + + @media screen and (max-width: 450px) { + .withdrawal-container { + .withdraw-form-wrapper { + .withdraw-form { + .deposit-wrapper-fields { + .btn-wrapper-deposit { + .holla-button { + width: -webkit-fill-available; + } + } + } + } + } + } + } +} diff --git a/web/src/containers/Deposit/index.js b/web/src/containers/Deposit/index.js index f38a60492c..71001a2783 100644 --- a/web/src/containers/Deposit/index.js +++ b/web/src/containers/Deposit/index.js @@ -8,10 +8,10 @@ import { BALANCE_ERROR } from 'config/constants'; import STRINGS from 'config/localizedStrings'; import { getCurrencyFromName } from 'utils/currency'; import { createAddress, cleanCreateAddress } from 'actions/userAction'; -import { NOTIFICATIONS } from 'actions/appActions'; +import { NOTIFICATIONS, depositCurrency } from 'actions/appActions'; import { DEFAULT_COIN_DATA } from 'config/constants'; import { openContactForm, setSnackNotification } from 'actions/appActions'; -import { MobileBarBack, Dialog, Notification } from 'components'; +import { MobileBarBack, Dialog, Notification, IconTitle } from 'components'; import { renderInformation, renderTitleSection, @@ -25,6 +25,7 @@ import RenderContent, { import { getWallet } from 'utils/wallet'; import QRCode from './QRCode'; import withConfig from 'components/ConfigProvider/withConfig'; +import strings from 'config/localizedStrings'; class Deposit extends Component { state = { @@ -36,16 +37,17 @@ class Deposit extends Component { formFields: {}, initialValues: {}, qrCodeOpen: false, + depositAddress: '', }; UNSAFE_componentWillMount() { - if (this.props.quoteData.error === BALANCE_ERROR) { + if (this.props?.quoteData?.error === BALANCE_ERROR) { this.setState({ depositPrice: this.props.quoteData.data.price }); } if (this.props.verification_level) { - this.validateRoute(this.props.routeParams.currency, this.props.coins); + this.validateRoute(this.props?.routeParams?.currency, this.props.coins); } - this.setCurrency(this.props.routeParams.currency); + this.setCurrency(this.props?.routeParams?.currency); } UNSAFE_componentWillReceiveProps(nextProps) { @@ -53,8 +55,11 @@ class Deposit extends Component { const { currency, networks } = this.state; if (!this.state.checked) { - if (nextProps.verification_level) { - this.validateRoute(nextProps.routeParams.currency, this.props.coins); + if ( + nextProps.verification_level && + nextProps.verification_level !== this.props.verification_level + ) { + this.validateRoute(nextProps.routeParams?.currency, this.props.coins); } } else if ( nextProps.selectedNetwork !== selectedNetwork || @@ -71,21 +76,59 @@ class Deposit extends Component { ); } - if (nextProps.routeParams.currency !== this.props.routeParams.currency) { - this.setCurrency(nextProps.routeParams.currency); + if (nextProps.routeParams?.currency !== this.props.routeParams?.currency) { + this.setCurrency(nextProps.routeParams?.currency); } if ( - nextProps.addressRequest.success === true && - nextProps.addressRequest.success !== this.props.addressRequest.success + nextProps.addressRequest?.success === true && + nextProps.addressRequest?.success !== this.props.addressRequest.success ) { this.onCloseDialog(); } } + componentDidUpdate(prevProps) { + const { wallet, getDepositCurrency } = this.props; + if (prevProps.wallet !== wallet) { + this.updateAddress(getDepositCurrency); + } + } + + componentWillUnmount() { + const { setDepositCurrency } = this.props; + setDepositCurrency(''); + } + + updateAddress = (selectedCurrency, hasNetwork = false) => { + const { wallet, getDepositCurrency, getDepositNetworkOptions } = this.props; + const depositAddress = wallet.filter((val) => { + if (hasNetwork) { + return ( + val.network === selectedCurrency && + val.currency === getDepositCurrency + ); + } else if (selectedCurrency) { + if (getDepositNetworkOptions) { + return ( + val.network === getDepositNetworkOptions && + val.currency === getDepositCurrency + ); + } else { + return val.currency === selectedCurrency; + } + } + return wallet; + }); + this.setState({ depositAddress: depositAddress[0]?.address }); + }; + setCurrency = (currencyName) => { + const { getDepositCurrency } = this.props; const currency = getCurrencyFromName(currencyName, this.props.coins); - if (currency) { + const isDeposit = + this.props?.router?.location?.pathname?.split('/')?.length === 3; + if (currency || (getDepositCurrency && !isDeposit)) { const { coins } = this.props; const coin = coins[currency]; const networks = coin.network && coin.network.split(','); @@ -104,7 +147,7 @@ class Deposit extends Component { () => { const { currency, initialNetwork, networks } = this.state; const { wallet, coins, verification_level } = this.props; - this.validateRoute(this.props.routeParams.currency, coins); + this.validateRoute(this.props.routeParams?.currency, coins); this.generateFormFields( currency, initialNetwork, @@ -115,14 +158,26 @@ class Deposit extends Component { ); } ); + } else if ( + this.props.isDepositAndWithdraw || + this.props.route.path === 'wallet/deposit' + ) { + return this.props.router?.push('/wallet/deposit'); } else { - this.props.router.push('/wallet'); + return this.props.router?.push('/wallet'); } }; validateRoute = (currency, coins) => { - if (!coins[currency]) { - this.props.router.push('/wallet'); + const { getDepositCurrency } = this.props; + if ( + (this.props.isDepositAndWithdraw || + this.props.route.path === 'wallet/withdraw') && + !getDepositCurrency + ) { + return this.props.router?.push('/wallet/deposit'); + } else if (!coins[currency]) { + return this.props.router?.push('/wallet'); } else if (currency) { this.setState({ checked: true }); } @@ -137,7 +192,7 @@ class Deposit extends Component { }; onGoBack = () => { - this.props.router.push('/wallet'); + return this.props.router?.push('/wallet'); }; onOpenDialog = () => { @@ -150,10 +205,29 @@ class Deposit extends Component { }; onCreateAddress = () => { - const { addressRequest, createAddress, selectedNetwork } = this.props; + const { + addressRequest, + createAddress, + selectedNetwork, + getDepositCurrency, + getDepositNetwork, + getDepositNetworkOptions, + coins, + } = this.props; const { currency } = this.state; - if (currency && !addressRequest.error) { - createAddress(currency, selectedNetwork); + const currentCurrency = getDepositCurrency ? getDepositCurrency : currency; + const network = getDepositNetworkOptions + ? getDepositNetworkOptions + : getDepositCurrency + ? getDepositNetwork + : selectedNetwork; + const hasNetwork = coins[currentCurrency]?.network; + if (hasNetwork) { + if (currentCurrency && !addressRequest.error) { + createAddress(currentCurrency, network); + } + } else if (currentCurrency && !addressRequest.error) { + createAddress(currentCurrency); } }; @@ -242,8 +316,8 @@ class Deposit extends Component { addressRequest, selectedNetwork, router, - wallet, orders, + getDepositCurrency, } = this.props; const { @@ -256,10 +330,19 @@ class Deposit extends Component { initialValues, showGenerateButton, qrCodeOpen, - networks, + depositAddress, } = this.state; - if (!id || !currency || !checked) { + const currentCurrency = getDepositCurrency ? getDepositCurrency : currency; + const isFiat = getDepositCurrency + ? coins[getDepositCurrency]?.type === 'fiat' + : coins[currency]?.type === 'fiat'; + + if ( + (!id || !currency || !checked) && + !this.props.isDepositAndWithdraw && + this.props.route.path !== 'wallet/deposit' + ) { return
; } @@ -267,22 +350,30 @@ class Deposit extends Component {
{isMobile && }
+ {!isMobile && !isFiat && ( + + )} {!isMobile && + isFiat && renderTitleSection( - currency, + currentCurrency, 'deposit', - ICONS['DEPOSIT_BITCOIN'], + ICONS['DEPOSIT'], coins, - 'DEPOSIT_BITCOIN' + 'DEPOSIT' )} -
+
-
- {renderBackToWallet()} +
+ {renderBackToWallet(this.onGoBack)} {openContactForm && + !isMobile && renderNeedHelpAction( openContactForm, constants.links, @@ -292,7 +383,7 @@ class Deposit extends Component {
@@ -328,14 +423,15 @@ class Deposit extends Component { showCloseText={true} style={{ 'z-index': 100 }} > - {dialogIsOpen && currency && ( + {dialogIsOpen && currentCurrency && ( )} @@ -351,8 +447,8 @@ class Deposit extends Component { {qrCodeOpen && ( )} @@ -374,6 +470,10 @@ const mapStateToProps = (store) => ({ selectedNetwork: formValueSelector('GenerateWalletForm')(store, 'network'), verification_level: store.user.verification_level, orders: store.order.activeOrders, + isDepositAndWithdraw: store.app.depositAndWithdraw, + getDepositCurrency: store.app.depositFields.depositCurrency, + getDepositNetwork: store.app.depositFields.depositNetwork, + getDepositNetworkOptions: store.app.depositFields.depositNetworkOptions, }); const mapDispatchToProps = (dispatch) => ({ @@ -381,6 +481,7 @@ const mapDispatchToProps = (dispatch) => ({ cleanCreateAddress: bindActionCreators(cleanCreateAddress, dispatch), openContactForm: bindActionCreators(openContactForm, dispatch), setSnackNotification: bindActionCreators(setSnackNotification, dispatch), + setDepositCurrency: bindActionCreators(depositCurrency, dispatch), }); export default connect( diff --git a/web/src/containers/Deposit/utils.js b/web/src/containers/Deposit/utils.js index 259f702615..00d8cb1a4a 100644 --- a/web/src/containers/Deposit/utils.js +++ b/web/src/containers/Deposit/utils.js @@ -4,16 +4,17 @@ import { Link } from 'react-router'; import { reduxForm } from 'redux-form'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import { isMobile } from 'react-device-detect'; -import { ExclamationCircleFilled } from '@ant-design/icons'; import { STATIC_ICONS } from 'config/icons'; -import STRINGS from 'config/localizedStrings'; import { EditWrapper, Button, SmartTarget } from 'components'; import { required } from 'components/Form/validations'; import { getNetworkNameByKey } from 'utils/wallet'; +import { renderLabel } from 'containers/Withdraw/utils'; +import STRINGS from 'config/localizedStrings'; import Image from 'components/Image'; -import renderFields from 'components/Form/factoryFields'; import Fiat from './Fiat'; +import DepositComponent from './Deposit'; +import TransactionsHistory from 'containers/TransactionsHistory'; export const generateBaseInformation = (id = '') => (
@@ -25,13 +26,13 @@ export const generateBaseInformation = (id = '') => (
); -export const renderBackToWallet = () => { +export const renderBackToWallet = (callback) => { return ( -
+
callback()}> {STRINGS.formatString( STRINGS['CURRENCY_WALLET.WALLET_PAGE'], - + {STRINGS['CURRENCY_WALLET.BACK']} )} @@ -40,6 +41,13 @@ export const renderBackToWallet = () => { ); }; +export const onHandleSymbol = (value) => { + const regex = /\(([^)]+)\)/; + const match = value.match(regex); + const curr = match ? match[1].toLowerCase() : null; + return curr; +}; + export const generateFormFields = ({ currency, networks, @@ -182,13 +190,17 @@ const RenderContentForm = ({ setCopied, copied, address, - showGenerateButton, - formFields, icons: ICONS, - selectedNetwork, targets, + depositCurrency, + currentCurrency, + openQRCode, + updateAddress, + depositAddress, + router, + selectedNetwork, }) => { - const coinObject = coins[currency]; + const coinObject = coins[depositCurrency] || coins[currency]; const generalId = 'REMOTE_COMPONENT__FIAT_WALLET_DEPOSIT'; const currencySpecificId = `${generalId}__${currency.toUpperCase()}`; @@ -196,7 +208,7 @@ const RenderContentForm = ({ ? currencySpecificId : generalId; - if (coinObject && coinObject.type !== 'fiat') { + if ((coinObject && coinObject.type !== 'fiat') || !coinObject) { return (
-
-
- - {titleSection} -
- {(currency === 'xrp' || - currency === 'xlm' || - selectedNetwork === 'xlm' || - selectedNetwork === 'ton') && ( -
-
- -
- - {STRINGS['DEPOSIT_FORM_TITLE_WARNING_DESTINATION_TAG']} - +
+
+ {!coinObject?.allow_deposit && currentCurrency && ( +
+
+
+ + {renderLabel('ACCORDIAN.DISABLED_DEPOSIT_CONTENT')} + +
+ )} + {currentCurrency && coinObject?.allow_deposit && ( +
+ + {titleSection}
+ )} + +
+ {!isMobile && ( +
+
)} - {renderFields(formFields)}
- {showGenerateButton && ( -
-
- )} - {isMobile && address && ( + {isMobile && address && depositAddress && (
+ ); } else if (coinObject && coinObject.type === 'fiat') { @@ -261,8 +283,14 @@ const RenderContentForm = ({ } }; -const mapStateToProps = ({ app: { targets } }) => ({ +const mapStateToProps = ({ + app: { + targets, + depositFields: { depositCurrency }, + }, +}) => ({ targets, + depositCurrency, }); const Form = reduxForm({ diff --git a/web/src/containers/DigitalAssets/components/AssetsWrapper.js b/web/src/containers/DigitalAssets/components/AssetsWrapper.js index 60e911c593..ca2147fa45 100644 --- a/web/src/containers/DigitalAssets/components/AssetsWrapper.js +++ b/web/src/containers/DigitalAssets/components/AssetsWrapper.js @@ -1,18 +1,19 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; +import { isMobile } from 'react-device-detect'; import { formatPercentage, formatToCurrency, countDecimals, } from 'utils/currency'; -import { isMobile } from 'react-device-detect'; import { SearchBox } from 'components'; import STRINGS from 'config/localizedStrings'; import { quicktradePairSelector } from 'containers/QuickTrade/components/utils'; import withConfig from 'components/ConfigProvider/withConfig'; import { getMiniCharts } from 'actions/chartAction'; import AssetsList from 'containers/DigitalAssets/components/AssetsList'; +import { RenderLoading } from './utils'; function onHandleInitialLoading(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -243,16 +244,20 @@ class AssetsWrapper extends Component { />
- pageSize} - /> + {data.length ? ( + pageSize} + /> + ) : ( + + )}
); } diff --git a/web/src/containers/DigitalAssets/components/utils.js b/web/src/containers/DigitalAssets/components/utils.js index bf02639916..cb45171be1 100644 --- a/web/src/containers/DigitalAssets/components/utils.js +++ b/web/src/containers/DigitalAssets/components/utils.js @@ -1,8 +1,13 @@ +import React from 'react'; +import { Spin } from 'antd'; +import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons'; import math from 'mathjs'; +import STRINGS from 'config/localizedStrings'; import { createSelector } from 'reselect'; import { DIGITAL_ASSETS_SORT } from 'actions/appActions'; import { unsortedMarketsSelector, getPairs } from 'containers/Trade/utils'; import { getPinnedAssets } from 'containers/Wallet/utils'; +import { EditWrapper } from 'components'; const getSortMode = (state) => state.app.digital_assets_sort.mode; const getSortDir = (state) => state.app.digital_assets_sort.is_descending; @@ -147,3 +152,67 @@ export const dataSelector = createSelector( return Object.values(data); } ); + +export const RenderLoading = () => { + const renderCaret = () => ( +
+ + +
+ ); + + return ( +
+
+
+ + {STRINGS['MARKETS_TABLE.ASSET']} + +
+
+ + {STRINGS['MARKETS_TABLE.TRADING_SYMBOL']} + +
+
+ + {STRINGS['MARKETS_TABLE.LAST_PRICE']} + +
+
+ + {STRINGS['MARKETS_TABLE.CHANGE_1D']} + + {renderCaret()} +
+
+ + {STRINGS['MARKETS_TABLE.CHANGE_7D']} + + {renderCaret()} +
+
+ + {STRINGS['MARKETS_TABLE.CHART_7D']} + +
+
+ + {STRINGS['TRADE_TAB_TRADE']} + +
+
+
+ +
+ + {STRINGS['DIGITAL_ASSETS.LOADING_PRICES']} + +
+
+ ); +}; diff --git a/web/src/containers/DigitalAssets/index.js b/web/src/containers/DigitalAssets/index.js index 160d9b45c1..c94220d2a2 100644 --- a/web/src/containers/DigitalAssets/index.js +++ b/web/src/containers/DigitalAssets/index.js @@ -44,23 +44,23 @@ const DigitalAssets = ({ pair, icons: ICONS, showQuickTrade }) => {
+ + + {STRINGS['ACCORDIAN.DEPOSIT']} + + {showQuickTrade && ( - + {STRINGS['DIGITAL_ASSETS.QUICK_TRADE']} )} - + {STRINGS['DIGITAL_ASSETS.PRO_TRADE']} - - - {STRINGS['DIGITAL_ASSETS.WALLET']} - -
diff --git a/web/src/containers/FeesAndLimits/TradingFees.js b/web/src/containers/FeesAndLimits/TradingFees.js index 1e09851c15..6a68561644 100644 --- a/web/src/containers/FeesAndLimits/TradingFees.js +++ b/web/src/containers/FeesAndLimits/TradingFees.js @@ -1,5 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; +import { ReactSVG } from 'react-svg'; +import { browserHistory } from 'react-router'; import { Select, Input } from 'antd'; import { SearchOutlined } from '@ant-design/icons'; import classnames from 'classnames'; @@ -19,6 +21,7 @@ const TradingFees = ({ icons: ICONS, search, setSearch, + user, }) => { const accountData = config_level[selectedLevel] || {}; const description = @@ -77,6 +80,29 @@ const TradingFees = ({
+ {user.discount > 0 && ( + <> +
+
+ +
+
+ {STRINGS['FEE_REDUCTION']}: {user.discount}% +
+
+
browserHistory.push('/referral')} + > + + {STRINGS['REFERRAL_LINK.GO_TO_REFERRAL']} + +
+ + )}
{ value: key, label: name, })), + user: state.user || {}, }; }; diff --git a/web/src/containers/P2P/P2PDash.js b/web/src/containers/P2P/P2PDash.js new file mode 100644 index 0000000000..92bb973184 --- /dev/null +++ b/web/src/containers/P2P/P2PDash.js @@ -0,0 +1,721 @@ +/* eslint-disable */ +import React, { useState, useEffect, useRef } from 'react'; +import { connect } from 'react-redux'; +import { ReactSVG } from 'react-svg'; +import { Switch, message } from 'antd'; +import { IconTitle, EditWrapper, Coin } from 'components'; +import STRINGS from 'config/localizedStrings'; +import { Button, Select, Input, InputNumber } from 'antd'; +// import { Link } from 'react-router'; +import withConfig from 'components/ConfigProvider/withConfig'; +import { + fetchDeals, + createTransaction, + fetchTransactions, +} from './actions/p2pActions'; +import { COUNTRIES_OPTIONS } from 'utils/countries'; +import { formatToCurrency } from 'utils/currency'; +import { isMobile } from 'react-device-detect'; +import classnames from 'classnames'; +import './_P2P.scss'; + +const P2PDash = ({ + data, + onClose, + coins, + pairs, + constants = {}, + icons: ICONS, + transaction_limits, + tiers = {}, + user, + setDisplayOrder, + refresh, + setSelectedTransaction, + p2p_config, + changeProfileTab, +}) => { + const [expandRow, setExpandRow] = useState(false); + const [selectedDeal, setSelectedDeal] = useState(); + const [selectedMethod, setSelectedMethod] = useState(); + const [deals, setDeals] = useState([]); + const [amountCurrency, setAmountCurrency] = useState(); + const [amountFiat, setAmountFiat] = useState(); + const [filterCoin, setFilterCoin] = useState(); + const [filterDigital, setFilterDigital] = useState(); + const [filterRegion, setFilterRegion] = useState(); + const [filterAmount, setFilterAmount] = useState(); + const [filterMethod, setFilterMethod] = useState(); + const [methods, setMethods] = useState([]); + const [loading, setLoading] = useState(false); + const inputRef = useRef(null); + useEffect(() => { + fetchDeals({ status: true }) + .then((res) => { + setDeals(res.data); + const newMethods = []; + + res.data.forEach((deal) => { + deal.payment_methods.forEach((method) => { + if (!newMethods.find((x) => x.system_name === method.system_name)) { + newMethods.push(method); + } + }); + }); + + setMethods(newMethods || []); + }) + .catch((err) => err); + }, [refresh]); + + const formatAmount = (currency, amount) => { + const min = coins[currency].min; + const formattedAmount = formatToCurrency(amount, min); + return formattedAmount; + }; + + const formatRate = (rate, spread, asset) => { + const amount = rate * (1 + Number(spread / 100 || 0)); + return formatAmount(asset, amount); + }; + + return ( +
+ {/*
+ + {STRINGS['P2P.I_WANT_TO_BUY']} + + + + + + {STRINGS['P2P.I_WANT_TO_SELL']} + +
*/} + {/*
+ Crypto + + {p2p_config?.digital_currencies.map((coin) => ( + + ))} + +
*/} + +
+
+ + {STRINGS['P2P.SPEND_FIAT_CURRENCY']} + + + + +
+
+ + {STRINGS['P2P.AMOUNT']} + + + { + setFilterAmount(e.target.value); + }} + placeholder={STRINGS['P2P.INPUT_FIAT_AMOUNT']} + /> + +
+ +
+ + {STRINGS['P2P.PAYMENT_METHOD']} + + + + +
+ +
+ + {STRINGS['P2P.AVAILABLE_REGIONS']} + + + + +
+
+
+
+ + + + + + + + + + + {deals + .filter( + (deal) => + (filterCoin ? filterCoin === deal.spending_asset : true) && + (filterDigital + ? filterDigital === deal.buying_asset + : true) && + (filterAmount ? filterAmount < deal.max_order_value : true) && + (filterMethod + ? deal.payment_methods.find( + (x) => x.system_name === filterMethod + ) + : true) && + (filterRegion ? filterRegion === deal.region : true) + ) + .map((deal) => { + return ( + <> + + + + + + + + {expandRow && expandRow && deal.id === selectedDeal.id && ( + + + + + + + + )} + + ); + })} + +
+ + {STRINGS['P2P.VENDOR']} + + + + {STRINGS['P2P.PRICE_LOWEST_FIRST']} + + + + {STRINGS['P2P.LIMIT_AVAILABLE']} + + + + {STRINGS['P2P.PAYMENT']} + + + + {STRINGS['P2P.TRADE']} + +
{ + setExpandRow(!expandRow); + setSelectedDeal(deal); + setAmountCurrency(); + setSelectedMethod(); + setAmountFiat(); + }} + className="td-fit" + > + +{' '} + + {deal.merchant.full_name || ( + + {STRINGS['P2P.ANONYMOUS']} + + )} + + { + setExpandRow(!expandRow); + setSelectedDeal(deal); + setAmountCurrency(); + setSelectedMethod(); + setAmountFiat(); + }} + className="td-fit" + > + {formatRate( + deal.exchange_rate, + deal.spread, + deal.spending_asset + )}{' '} + {deal.spending_asset.toUpperCase()} + { + setExpandRow(!expandRow); + setSelectedDeal(deal); + setAmountCurrency(); + setSelectedMethod(); + setAmountFiat(); + }} + > +
+ + {STRINGS['P2P.AVAILABLE']} + + : {deal.total_order_amount}{' '} + {deal.buying_asset.toUpperCase()} +
+
+ + {STRINGS['P2P.LIMIT']} + + : {deal.min_order_value} - {deal.max_order_value}{' '} + {deal.spending_asset.toUpperCase()} +
+
{ + setExpandRow(!expandRow); + setSelectedDeal(deal); + setAmountCurrency(); + setSelectedMethod(); + setAmountFiat(); + }} + > + {deal.payment_methods + .map((method) => method.system_name) + .join(', ')} + + {!( + expandRow && + expandRow && + deal.id === selectedDeal.id + ) && ( +
+ +
+ )} +
+
{ + changeProfileTab(deal.merchant); + }} + style={{ + position: 'relative', + bottom: 40, + cursor: 'pointer', + }} + > + ( + + {STRINGS['P2P.VIEW_PROFILE']} + + ) +
+ + {STRINGS['P2P.PAYMENT_TIME_LIMIT']} + +
+ + {STRINGS['P2P.TERMS_CONDITIONS']} + + : {deal.terms} +
+
+
+
+ + {STRINGS['P2P.SELECT_PAYMENT_METHOD']} + + + + +
+ +
+ + + {STRINGS['P2P.SPEND_AMOUNT']} + {' '} + ({deal.spending_asset.toUpperCase()}) + + + + { + setAmountFiat(e); + const currencyAmount = + Number(e) / + Number( + deal.exchange_rate * + (1 + Number(deal.spread / 100 || 0)) + ); + + const formatted = formatAmount( + deal.buying_asset, + currencyAmount + ); + + setAmountCurrency(formatted); + }} + placeholder={deal.spending_asset.toUpperCase()} + /> + +
+
+ + + {STRINGS['P2P.AMOUNT_TO_RECEIVE']} + {' '} + ({deal.buying_asset.toUpperCase()}) + + + + + +
+
+ + + +
+
+
+
+
+ ); +}; + +const mapStateToProps = (state) => ({ + pairs: state.app.pairs, + coins: state.app.coins, + constants: state.app.constants, + transaction_limits: state.app.transaction_limits, + user: state.user, + p2p_config: state.app.constants.p2p_config, +}); + +export default connect(mapStateToProps)(withConfig(P2PDash)); diff --git a/web/src/containers/P2P/P2PMyDeals.js b/web/src/containers/P2P/P2PMyDeals.js new file mode 100644 index 0000000000..0a966eab59 --- /dev/null +++ b/web/src/containers/P2P/P2PMyDeals.js @@ -0,0 +1,290 @@ +/* eslint-disable */ +import React, { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +// import { ReactSVG } from 'react-svg'; + +import { IconTitle, EditWrapper } from 'components'; +import STRINGS from 'config/localizedStrings'; +import withConfig from 'components/ConfigProvider/withConfig'; +import { Button, Checkbox, message } from 'antd'; +import { fetchDeals, editDeal } from './actions/p2pActions'; +import { formatToCurrency } from 'utils/currency'; +import { isMobile } from 'react-device-detect'; +import classnames from 'classnames'; +import './_P2P.scss'; +const P2PMyDeals = ({ + data, + onClose, + coins, + pairs, + constants = {}, + icons: ICONS, + transaction_limits, + tiers = {}, + user, + refresh, + setSelectedDealEdit, + setTab, +}) => { + const [myDeals, setMyDeals] = useState([]); + const [checks, setCheks] = useState([]); + useEffect(() => { + fetchDeals({ user_id: user.id }) + .then((res) => { + setMyDeals(res.data); + }) + .catch((err) => err); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [refresh]); + + const formatAmount = (currency, amount) => { + const min = coins[currency].min; + const formattedAmount = formatToCurrency(amount, min); + return formattedAmount; + }; + + const formatRate = (rate, spread, asset) => { + const amount = rate * (1 + Number(spread / 100 || 0)); + return formatAmount(asset, amount); + }; + + return ( +
+
+ + { + if (e.target.checked) { + setCheks(myDeals.map((deal) => deal.id)); + } else { + setCheks([]); + } + }} + style={{ position: 'relative', top: 5 }} + className="whiteTextP2P" + > + {myDeals.length === 0 ? ( + + {STRINGS['P2P.NO_DEALS']} + + ) : ( + + {myDeals.length} {STRINGS['P2P.NUM_DEALS']} + + )} + + + + + + + + +
+ +
+ + + + + + + + + + + + + + {myDeals.map((deal) => { + return ( + + + + + + + + + + + + ); + })} + +
+ + {STRINGS['P2P.EDIT']} + + + + {STRINGS['P2P.SIDE']} + + + + {STRINGS['P2P.STATUS']} + + + + {STRINGS['P2P.PRICE_DISPLAYED']} + + + + {STRINGS['P2P.LIMIT_AVAILABLE']} + + + + {STRINGS['P2P.PAYMENT']} + + + + {STRINGS['P2P.EDIT_DEAL']} + +
+ id === deal.id)} + onChange={(e) => { + if (e.target.checked) { + if (!checks.find((id) => id === deal.id)) + setCheks([...checks, deal.id]); + } else { + setCheks(checks.filter((id) => id !== deal.id)); + } + }} + /> + + + + {deal.status ? ( + + {STRINGS['P2P.ACTIVE']} + + ) : ( + + {STRINGS['P2P.INACTIVE']} + + )} + + {formatRate( + deal.exchange_rate, + deal.spread, + deal.spending_asset + )}{' '} + {deal.spending_asset.toUpperCase()} + +
+ + {STRINGS['P2P.AVAILABLE']} + + : {deal.total_order_amount}{' '} + {deal.buying_asset.toUpperCase()} +
+
+ + {STRINGS['P2P.LIMIT']} + + : {deal.min_order_value} - {deal.max_order_value}{' '} + {deal.spending_asset.toUpperCase()} +
+
+ {deal.payment_methods + .map((method) => method.system_name) + .join(', ')} + + +
+
+
+ ); +}; + +const mapStateToProps = (state) => ({ + pairs: state.app.pairs, + coins: state.app.coins, + constants: state.app.constants, + transaction_limits: state.app.transaction_limits, + user: state.user, +}); + +export default connect(mapStateToProps)(withConfig(P2PMyDeals)); diff --git a/web/src/containers/P2P/P2POrder.js b/web/src/containers/P2P/P2POrder.js new file mode 100644 index 0000000000..dd554ce19c --- /dev/null +++ b/web/src/containers/P2P/P2POrder.js @@ -0,0 +1,1653 @@ +/* eslint-disable */ +import React, { useEffect, useState, useRef } from 'react'; +import { connect } from 'react-redux'; +import { ReactSVG } from 'react-svg'; + +import { IconTitle, EditWrapper } from 'components'; +import STRINGS from 'config/localizedStrings'; +import withConfig from 'components/ConfigProvider/withConfig'; +import { Button, Input, message, Modal, Rate, Tooltip } from 'antd'; +import moment from 'moment'; +import { + createChatMessage, + fetchTransactions, + updateTransaction, + createFeedback, + fetchFeedback, +} from './actions/p2pActions'; +import { withRouter } from 'react-router'; +import { formatToCurrency } from 'utils/currency'; +import { getToken } from 'utils/token'; +import { WS_URL } from 'config/constants'; +import { CloseOutlined } from '@ant-design/icons'; +import { isMobile } from 'react-device-detect'; +import classnames from 'classnames'; +import BigNumber from 'bignumber.js'; +import './_P2P.scss'; + +const P2POrder = ({ + data, + onClose, + coins, + pairs, + constants = {}, + icons: ICONS, + transaction_limits, + tiers = {}, + setDisplayOrder, + selectedTransaction, + setSelectedTransaction, + user, + router, + p2p_config, +}) => { + const coin = coins[selectedTransaction.deal.buying_asset]; + const [selectedOrder, setSelectedOrder] = useState(selectedTransaction); + const [chatMessage, setChatMessage] = useState(); + const [appealReason, setAppealReason] = useState(); + const [feedback, setFeedback] = useState(); + const [rating, setRating] = useState(); + const [appealSide, setAppealSide] = useState(); + const [displayAppealModal, setDisplayAppealModel] = useState(false); + const [displayFeedbackModal, setDisplayFeedbackModel] = useState(false); + const [hasFeedback, setHasFeedback] = useState(false); + const [ws, setWs] = useState(); + const [ready, setReady] = useState(false); + const [displayCancelWarning, setDisplayCancelWarning] = useState(); + const [displayConfirmWarning, setDisplayConfirmWarning] = useState(); + const [lastClickTime, setLastClickTime] = useState(0); + const ref = useRef(null); + const buttonRef = useRef(null); + + useEffect(() => { + ref.current.scroll({ + top: 9999, + behavior: 'smooth', + }); + }, [selectedOrder.messages]); + + const handleKeyDown = (event) => { + if (event.key === 'Enter') { + buttonRef.current.click(); + } + }; + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + + // Cleanup the event listener on component unmount + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, []); + + useEffect(() => { + const url = `${WS_URL}/stream?authorization=Bearer ${getToken()}`; + const p2pWs = new WebSocket(url); + + p2pWs.onopen = (evt) => { + setWs(p2pWs); + setReady(true); + p2pWs.send( + JSON.stringify({ + op: 'subscribe', + args: [`p2pChat:${selectedTransaction.id}`], + }) + ); + setInterval(() => { + p2pWs.send( + JSON.stringify({ + op: 'ping', + }) + ); + }, 55000); + }; + + p2pWs.onmessage = (evt) => { + const data = JSON.parse(evt.data); + switch (data.action) { + case 'addMessage': { + if (data.data) { + const { id } = data.data; + if (selectedOrder.id === id) { + setSelectedOrder((prevState) => { + const messages = [...prevState.messages]; + + messages.push(data.data); + + return { + ...prevState, + messages, + }; + }); + } + } + break; + } + + case 'getStatus': { + fetchTransactions({ id: selectedOrder.id }) + .then((transaction) => { + if (transaction.data[0].transaction_status === 'complete') { + setHasFeedback(false); + } + setSelectedOrder(transaction.data[0]); + }) + .catch((err) => err); + break; + } + + default: + break; + } + }; + + return () => { + p2pWs.send( + JSON.stringify({ + op: 'unsubscribe', + args: [`p2pChat:${selectedTransaction.id}`], + }) + ); + p2pWs.close(); + }; + }, []); + + useEffect(() => { + getTransaction(); + fetchFeedback({ transaction_id: selectedOrder.id }) + .then((res) => { + if (res?.data?.length > 0) { + setHasFeedback(true); + } + }) + .catch((err) => err); + }, []); + + const getTransaction = async () => { + try { + const transaction = await fetchTransactions({ + id: selectedOrder.id, + }); + setSelectedOrder(transaction.data[0]); + } catch (error) { + return error; + } + }; + + const addMessage = (message) => { + ws.send( + JSON.stringify({ + op: 'p2pChat', + args: [ + { + action: 'addMessage', + data: message, + }, + ], + }) + ); + }; + + const updateStatus = (status) => { + ws.send( + JSON.stringify({ + op: 'p2pChat', + args: [ + { + action: 'getStatus', + data: { + id: selectedOrder.id, + status, + }, + }, + ], + }) + ); + }; + + const userReceiveAmount = () => { + const incrementUnit = + coins?.[selectedOrder.deal.buying_asset]?.increment_unit; + const buyerFeeAmount = new BigNumber(selectedOrder?.amount_digital_currency) + .multipliedBy(p2p_config?.user_fee) + .dividedBy(100) + .toNumber(); + + const decimalPoint = new BigNumber(incrementUnit).dp(); + const sourceAmount = new BigNumber( + selectedOrder?.amount_digital_currency - buyerFeeAmount + ) + .decimalPlaces(decimalPoint) + .toNumber(); + return sourceAmount; + }; + + const sendChatMessage = async () => { + const now = Date.now(); + if (now - lastClickTime >= 1000 && chatMessage?.trim()?.length > 0) { + try { + await createChatMessage({ + receiver_id: + user.id === selectedOrder?.merchant_id + ? selectedOrder?.user_id + : selectedOrder?.merchant_id, + message: chatMessage, + transaction_id: selectedOrder.id, + }); + + addMessage({ + sender_id: user.id, + type: 'message', + receiver_id: + user.id === selectedOrder?.merchant_id + ? selectedOrder?.user_id + : selectedOrder?.merchant_id, + message: chatMessage, + id: selectedOrder.id, + }); + + setChatMessage(); + } catch (error) { + message.error(error.data.message); + } + setLastClickTime(now); + } + }; + + return ( + <> + } + className="stake_table_theme stake_theme" + bodyStyle={{}} + visible={displayAppealModal} + width={450} + footer={null} + onCancel={() => { + setDisplayAppealModel(false); + }} + > +
+
+

+ + {STRINGS['P2P.APPEAL_TRANSACTION']} + +

+
+
+
+ + {STRINGS['P2P.ENTER_REASON']} + +
+ { + setAppealReason(e.target.value); + }} + /> +
+
+ +
+ + +
+
+ + {displayFeedbackModal && ( + } + className="stake_table_theme stake_theme" + bodyStyle={{}} + visible={displayFeedbackModal} + width={450} + footer={null} + onCancel={() => { + setDisplayFeedbackModel(false); + }} + > +
+
+

+ + {STRINGS['P2P.SUBMIT_FEEDBACK']} + +

+
+
+
+ + {STRINGS['P2P.INPUT_FEEDBACK']} + +
+ { + setFeedback(e.target.value); + }} + /> +
+
+
+ + {STRINGS['P2P.SELECT_RATING']} + +
+ { + if (e > 0) setRating(e); + }} + value={rating} + /> +
+
+ +
+ + +
+
+ )} + + {displayCancelWarning && ( + } + className="stake_table_theme stake_theme" + bodyStyle={{}} + visible={displayCancelWarning} + width={450} + footer={null} + onCancel={() => { + setDisplayCancelWarning(false); + }} + > +
+
+

+ + {STRINGS['P2P.CANCEL_WARNING']} + +

+
+
+ +
+ + +
+
+ )} + + {displayConfirmWarning && ( + } + className="stake_table_theme stake_theme" + bodyStyle={{}} + visible={displayConfirmWarning} + width={450} + footer={null} + onCancel={() => { + setDisplayConfirmWarning(false); + }} + > +
+
+

+ + {STRINGS['P2P.CONFIRM_WARNING']} + +

+

+ {userReceiveAmount()}{' '} + {selectedOrder?.deal?.buying_asset?.toUpperCase()} will be + released from your balance +

+
+
+ +
+ + +
+
+ )} + +
{ + setDisplayOrder(false); + router.push('/p2p'); + }} + style={{ + marginBottom: 10, + cursor: 'pointer', + textDecoration: 'underline', + }} + > + {STRINGS['P2P.BACK']} +
+
+
+
+
+
+
+ + {STRINGS['P2P.ORDER']} + +
+
+ {user.id === selectedOrder.merchant_id ? ( + + {STRINGS['P2P.SELL_COIN']} + + ) : ( + + {STRINGS['P2P.BUY_COIN']} + + )}{' '} + {coin?.fullname?.toUpperCase()} ({coin?.symbol?.toUpperCase()} + ) +
+
+
+
+
+ + {STRINGS['P2P.TRANSACTION_ID']} + + {': '} + {selectedOrder.transaction_id} +
+ +
+
+
+ + {STRINGS['P2P.AMOUNT_TO']} + {' '} + {user.id === selectedOrder?.merchant_id + ? STRINGS['P2P.RELEASE'] + : STRINGS['P2P.SEND_UPPER']} + : +
+
+ {user.id === selectedOrder?.merchant_id && ( +
+ {userReceiveAmount()}{' '} + {selectedOrder?.deal?.buying_asset?.toUpperCase()} +
+ )} + {user.id === selectedOrder?.user_id && ( +
+ {selectedOrder?.amount_fiat}{' '} + {selectedOrder?.deal?.spending_asset?.toUpperCase()} +
+ )} +
+ {user.id === selectedOrder?.merchant_id ? ( + + {STRINGS['P2P.AMOUNT_SEND_RELEASE']} + + ) : ( + + {STRINGS['P2P.REQUIRED_FLAT_TRANSFER_AMOUNT']} + + )} +
+
+
+
+
+
+ + {STRINGS['P2P.PRICE']} + + : +
+
+
+ {selectedOrder?.price}{' '} + {selectedOrder?.deal?.spending_asset?.toUpperCase()} +
+
+ + {STRINGS['P2P.PER_COIN']} + {' '} + {selectedOrder?.deal?.buying_asset?.toUpperCase()} +
+
+
+
+
+
+ + {STRINGS['P2P.RECEIVING_AMOUNT']} + + : +
+ {user.id === selectedOrder?.merchant_id && ( +
+
+ {selectedOrder?.amount_fiat}{' '} + {selectedOrder?.deal?.spending_asset?.toUpperCase()} +
+
+ {selectedOrder?.deal?.spending_asset?.toUpperCase()}{' '} + + {STRINGS['P2P.SPENDING_AMOUNT']} + +
+
+ )} + + {user.id === selectedOrder?.user_id && ( +
+
+ {userReceiveAmount()}{' '} + {selectedOrder?.deal?.buying_asset?.toUpperCase()} +
+
+ {selectedOrder?.deal?.buying_asset?.toUpperCase()}{' '} + + {STRINGS['P2P.BUYING_AMOUNT']} + +
+
+ )} +
+
+
+
+ + {STRINGS['P2P.FEE']} + + : +
+ {user.id === selectedOrder?.merchant_id && ( +
+
{p2p_config?.merchant_fee}%
+
+ )} + + {user.id === selectedOrder?.user_id && ( +
+
{p2p_config?.user_fee}%
+
+ )} +
+
+ +
+
+ + {STRINGS['P2P.TRANSFER_DETAILS']} + +
+ {user.id === selectedOrder?.user_id && ( +
+ + {STRINGS['P2P.PAYMENT_INSTRUCTIONS']} + +
+ )} + + {user.id === selectedOrder?.merchant_id && ( +
+ + {STRINGS['P2P.PAYMENT_ACCOUNT']} + +
+ )} + +
+
+
+ + {STRINGS['P2P.PAYMENT_METHOD']} + + : +
+
+ {selectedOrder?.payment_method_used?.system_name} +
+
+ + {selectedOrder?.payment_method_used?.fields?.map((x) => { + return ( +
+
{x?.name}:
+
{x?.value}
+
+ ); + })} +
+
+ +
+
+ + {STRINGS['P2P.EXPECTED_TIME']} + +
+ + {user.id === selectedOrder?.user_id && ( + <> + {selectedOrder.user_status === 'pending' && ( + <> +
+ + {STRINGS['P2P.PAYMENT_TIME']} + +
+
+ + {STRINGS['P2P.ORDER_CANCELLED']} + +
+ + )} + + {selectedOrder.user_status === 'confirmed' && ( +
+ + {STRINGS['P2P.FUNDS_CREDITED']} + +
+ )} + + {selectedOrder.merchant_status === 'cancelled' && ( +
+ + {STRINGS['P2P.VENDOR_CANCELLED']} + +
+ )} + + {selectedOrder.merchant_status === 'confirmed' && ( +
+
+ + {STRINGS['P2P.ORDER_COMPLETE']} + +
+
+ + {STRINGS['P2P.FUNDS_TRANSFERRED']} + +
+
{ + router.replace('/transactions?tab=deposits'); + }} + > + + + {STRINGS['P2P.GO_DEPOSIT']} + + +
+ {!hasFeedback && ( + + )} +
+ )} + {selectedOrder.merchant_status === 'appeal' && ( + <> +
+ + {STRINGS['P2P.VENDOR_APPEALED']} + +
+ + )} + {selectedOrder.user_status === 'appeal' && ( + <> +
+ + {STRINGS['P2P.USER_APPEALED']} + +
+ + )} + + )} + + {user.id === selectedOrder?.merchant_id && ( + <> + {selectedOrder.merchant_status === 'confirmed' && ( +
+
+ + {STRINGS['P2P.ORDER_COMPLETE']} + +
+
+ + {STRINGS['P2P.ORDER_COMPLETE_VENDOR']} + +
+
{ + router.replace('/transactions?tab=withdrawals'); + }} + > + + + {STRINGS['P2P.GO_WITHDRAWALS']} + + +
+
+ )} + + {selectedOrder.user_status === 'pending' && ( + <> +
+ + {STRINGS['P2P.PAYMENT_NOT_SENT']} + +
+
+ + {STRINGS['P2P.CONFIRM_AND_RELEASE']} + +
+ + )} + {selectedOrder.user_status === 'cancelled' && ( + <> +
+ + {STRINGS['P2P.TRANSACTION_CANCELLED']} + +
+ + )} + {selectedOrder.user_status === 'confirmed' && + selectedOrder?.merchant_status !== 'confirmed' && ( + <> +
+ + {STRINGS['P2P.BUYER_CONFIRMED']} + +
+
+ + {STRINGS['P2P.CHECK_AND_RELEASE']} + +
+ + )} + {user.id === selectedOrder.user_id && + selectedOrder.user_status === 'appeal' && ( + <> +
+ + {STRINGS['P2P.USER_APPEALED']} + +
+ + )} + + {user.id === selectedOrder.merchant_id && + selectedOrder.user_status === 'appeal' && ( + <> +
+ + {STRINGS['P2P.BUYER_APPEALED_ORDER']} + +
+ + )} + + )} + +
+ {user.id === selectedOrder?.user_id && ( + <> + {selectedOrder.user_status === 'confirmed' && + selectedOrder.merchant_status === 'pending' && ( + <> +
{ + try { + setDisplayAppealModel(true); + setAppealSide('user'); + } catch (error) { + message.error(error.data.message); + } + }} + style={{ + textDecoration: 'underline', + cursor: 'pointer', + position: 'relative', + top: 5, + }} + > + + {STRINGS['P2P.APPEAL']} + +
+
{ + setDisplayCancelWarning(true); + }} + style={{ + textDecoration: 'underline', + cursor: 'pointer', + position: 'relative', + top: 5, + }} + > + + {STRINGS['P2P.CANCEL_ORDER']} + +
+ + )} + + )} + + {user.id === selectedOrder?.merchant_id && + selectedOrder?.merchant_status === 'pending' && ( + +
{ + try { + setDisplayAppealModel(true); + setAppealSide('merchant'); + } catch (error) { + message.error(error.data.message); + } + }} + style={{ + textDecoration: 'underline', + cursor: 'pointer', + position: 'relative', + top: 5, + }} + > + + {STRINGS['P2P.APPEAL']} + +
+ + + + +
+ )} + {user.id === selectedOrder?.merchant_id && + selectedOrder?.merchant_status === 'appeal' && ( +
+ + {STRINGS['P2P.USER_APPEALED']} + +
+ )} +
+
+
+
+
+ {user.id === selectedOrder?.merchant_id ? ( + + {STRINGS['P2P.CHAT_WITH_USER']} + + ) : ( + + {STRINGS['P2P.CHAT_WITH_VENDOR']} + + )} +
+
+
+ {user.id === selectedOrder?.merchant_id ? ( + + {STRINGS['P2P.USER_NAME']} + + ) : ( + + {STRINGS['P2P.VENDOR_NAME']} + + )}{' '} + {user.id === selectedOrder?.merchant_id + ? selectedOrder?.buyer?.full_name || ( + + {STRINGS['P2P.ANONYMOUS']} + + ) + : selectedOrder?.merchant?.full_name || ( + + {STRINGS['P2P.ANONYMOUS']} + + )} +
+
+
+ {user.id === selectedOrder?.user_id && ( +
+ + {STRINGS['P2P.ORDER_INITIATED']} + {' '} + {selectedOrder?.merchant?.full_name || ( + + {STRINGS['P2P.ANONYMOUS']} + + )}{' '} + ( + {moment(selectedOrder?.created_at).format( + 'DD/MMM/YYYY, hh:mmA' + )} + ). +
+ )} + + {user.id === selectedOrder?.user_id && ( +
+ + {STRINGS['P2P.CONFIRM_PAYMENT']} + +
+ )} + + {user.id === selectedOrder?.merchant_id && ( +
+ + {STRINGS['P2P.ORDER_INITIATED_VENDOR']} + {' '} + {selectedOrder?.buyer?.full_name || ( + + {STRINGS['P2P.ANONYMOUS']} + + )}{' '} + ( + {moment(selectedOrder?.created_at).format( + 'DD/MMM/YYYY, hh:mmA' + )} + ). +
+ )} + {user.id === selectedOrder?.merchant_id && ( +
+ + {STRINGS['P2P.CONFIRM_PAYMENT_VENDOR']} + +
+ )} +
+ +
+
+ {selectedOrder?.messages.map((message, index) => { + if (index === 0) { + return ( +
+
{selectedOrder?.merchant?.full_name}:
+
{message.message}
+
+ {moment(message?.created_at || new Date()).format( + 'DD/MMM/YYYY, hh:mmA ' + )} +
+
+ ); + } else { + if (message.type === 'notification') { + return ( +
+ {message.message === 'BUYER_PAID_ORDER' && + user.id === selectedOrder.user_id ? ( + + {STRINGS[`P2P.BUYER_SENT_FUNDS`]} + + ) : ( + + {STRINGS[`P2P.${message.message}`]} + + )}{' '} + ( + {moment(message?.created_at || new Date()).format( + 'DD/MMM/YYYY, hh:mmA' + )} + ) +
+ ); + } else { + if (message.sender_id === user.id) { + return ( +
+
+ + {STRINGS['P2P.YOU']} + + : +
+
{message.message}
+
+ {moment( + message?.created_at || new Date() + ).format('DD/MMM/YYYY, hh:mmA ')} +
+
+ ); + } else { + return ( +
+
+ {message.receiver_id === + selectedOrder.merchant_id + ? STRINGS['P2P.BUYER'] + : selectedOrder?.merchant?.full_name} + : +
+
{message.message}
+
+ {moment( + message?.created_at || new Date() + ).format('DD/MMM/YYYY, hh:mmA ')} +
+
+ ); + } + } + } + })} +
+
+ +
+
+
+ { + setChatMessage(e.target.value); + }} + /> +
+
+ + {STRINGS['P2P.SEND_UPPER']} + +
+
+
+
+
+
+
+ + {user.id === selectedOrder?.user_id && + selectedOrder?.transaction_status === 'active' && + selectedOrder.user_status === 'pending' && ( +
+ + +
+ )} + + ); +}; + +const mapStateToProps = (state) => ({ + pairs: state.app.pairs, + coins: state.app.coins, + constants: state.app.constants, + transaction_limits: state.app.transaction_limits, + user: state.user, + p2p_config: state.app.constants.p2p_config, +}); + +export default connect(mapStateToProps)(withRouter(withConfig(P2POrder))); diff --git a/web/src/containers/P2P/P2POrders.js b/web/src/containers/P2P/P2POrders.js new file mode 100644 index 0000000000..5b557eb393 --- /dev/null +++ b/web/src/containers/P2P/P2POrders.js @@ -0,0 +1,269 @@ +/* eslint-disable */ +import React, { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +// import { ReactSVG } from 'react-svg'; + +import { IconTitle, EditWrapper } from 'components'; +import STRINGS from 'config/localizedStrings'; +import withConfig from 'components/ConfigProvider/withConfig'; +import { Button, Radio } from 'antd'; +import { fetchTransactions } from './actions/p2pActions'; +import { withRouter } from 'react-router'; +import { formatToCurrency } from 'utils/currency'; +import { isMobile } from 'react-device-detect'; +import classnames from 'classnames'; +import './_P2P.scss'; +const P2POrders = ({ + data, + onClose, + coins, + pairs, + constants = {}, + icons: ICONS, + transaction_limits, + tiers = {}, + setDisplayOrder, + setSelectedTransaction, + refresh, + user, + router, + changeProfileTab, +}) => { + const [transactions, setTransactions] = useState([]); + const [filter, setFilter] = useState(); + const [option, setOption] = useState('2'); + + useEffect(() => { + fetchTransactions() + .then((res) => { + setTransactions(res.data); + }) + .catch((err) => err); + }, [refresh]); + + const formatAmount = (currency, amount) => { + const min = coins[currency].min; + const formattedAmount = formatToCurrency(amount, min); + return formattedAmount; + }; + + return ( +
+
+ setOption(e.target.value)}> + { + setFilter('active'); + }} + > + + {STRINGS['P2P.PROCESSING']} + + + { + setFilter(); + }} + > + + {STRINGS['P2P.ALL_ORDERS']} + + + +
+ +
+ + + + + + + + + + + + + + {transactions + .filter((x) => + filter + ? ['active', 'appealed'].includes(x.transaction_status) + : true + ) + .map((transaction) => { + return ( + + + + + + + + + + + + ); + })} + +
+ + {STRINGS['P2P.TYPE_COIN']} + + + + {STRINGS['P2P.FIAT_AMOUNT']} + + + + {STRINGS['P2P.PRICE']} + + + + {STRINGS['P2P.CRYPTO_AMOUNT']} + + + + {STRINGS['P2P.COUNTERPARTY']} + + + + {STRINGS['P2P.STATUS']} + + + + {STRINGS['P2P.OPERATION']} + +
+ {transaction?.user_id === user.id ? ( + + ) : ( + + )} + + {transaction?.amount_fiat}{' '} + {transaction?.deal?.spending_asset?.toUpperCase()} + + {transaction?.price}{' '} + {transaction?.deal?.buying_asset?.toUpperCase()} + + {formatAmount( + transaction?.deal?.buying_asset, + transaction?.amount_digital_currency + )}{' '} + {transaction?.deal?.buying_asset?.toUpperCase()} + + {transaction?.user_id === user.id ? ( + { + changeProfileTab(transaction?.merchant); + }} + > + {transaction?.merchant?.full_name || ( + + {STRINGS['P2P.ANONYMOUS']} + + )} + + ) : ( + + {transaction?.buyer?.full_name || ( + + {STRINGS['P2P.ANONYMOUS']} + + )} + + )} + + {transaction?.transaction_status?.toUpperCase()} + +
{ + setDisplayOrder(true); + setSelectedTransaction(transaction); + router.replace(`/p2p/order/${transaction.id}`); + }} + style={{ + display: 'flex', + justifyContent: 'flex-end', + cursor: 'pointer', + }} + className="purpleTextP2P" + > + + {STRINGS['P2P.VIEW_ORDER']} + +
+
+
+
+ ); +}; + +const mapStateToProps = (state) => ({ + pairs: state.app.pairs, + coins: state.app.coins, + constants: state.app.constants, + transaction_limits: state.app.transaction_limits, + router: state.router, + user: state.user, +}); + +export default connect(mapStateToProps)(withRouter(withConfig(P2POrders))); diff --git a/web/src/containers/P2P/P2PPostDeal.js b/web/src/containers/P2P/P2PPostDeal.js new file mode 100644 index 0000000000..b6b1d14a6e --- /dev/null +++ b/web/src/containers/P2P/P2PPostDeal.js @@ -0,0 +1,884 @@ +/* eslint-disable */ +import React, { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +import { ReactSVG } from 'react-svg'; +import { Button, Steps, message, Modal } from 'antd'; +import { IconTitle, EditWrapper } from 'components'; +import STRINGS from 'config/localizedStrings'; +import withConfig from 'components/ConfigProvider/withConfig'; +import { Switch, Select, Input } from 'antd'; +import { postDeal, editDeal } from './actions/p2pActions'; +import { CloseOutlined } from '@ant-design/icons'; +import { formatToCurrency } from 'utils/currency'; +import { COUNTRIES_OPTIONS } from 'utils/countries'; +import { isMobile } from 'react-device-detect'; +import classnames from 'classnames'; +import BigNumber from 'bignumber.js'; +import './_P2P.scss'; + +const P2PPostDeal = ({ + data, + onClose, + coins, + pairs, + constants = {}, + icons: ICONS, + transaction_limits, + tiers = {}, + p2p_config, + setTab, + setRefresh, + refresh, + selectedDealEdit, + setSelectedDealEdit, +}) => { + const [step, setStep] = useState(1); + + const [priceType, setPriceType] = useState('static'); + const [buyingAsset, setBuyingAsset] = useState(); + const [spendingAsset, setSpendingAsset] = useState(); + const [exchangeRate, setExchangeRate] = useState(); + const [spread, setSpread] = useState(); + const [totalOrderAmount, setTotalOrderAmount] = useState(); + const [minOrderValue, setMinOrderValue] = useState(); + const [maxOrderValue, setMaxOrderValue] = useState(); + const [terms, setTerms] = useState(); + const [autoResponse, setAutoResponse] = useState(); + const [paymentMethods, setPaymentMethods] = useState([]); + const [selectedMethod, setSelectedMethod] = useState({}); + const [addMethodDetails, setAddMethodDetails] = useState(); + const [region, setRegion] = useState(); + + const dataSte = [ + { + title: STRINGS['P2P.STEP_SET_TYPE_PRICE'], + }, + { + title: STRINGS['P2P.STEP_SET_TOTAL_AMOUNT_PAYMENT_METHODS'], + }, + { + title: STRINGS['P2P.STEP_SET_TERMS_AUTO_RESPONSE'], + }, + ]; + const { Step } = Steps; + + useEffect(() => { + if (selectedDealEdit) { + setPriceType(selectedDealEdit?.price_type); + setBuyingAsset(selectedDealEdit?.buying_asset); + setSpendingAsset(selectedDealEdit?.spending_asset); + setExchangeRate(selectedDealEdit?.exchange_rate); + setSpread(selectedDealEdit?.spread); + setTotalOrderAmount(selectedDealEdit?.total_order_amount); + setMinOrderValue(selectedDealEdit?.min_order_value); + setMaxOrderValue(selectedDealEdit?.max_order_value); + setTerms(selectedDealEdit?.terms); + setAutoResponse(selectedDealEdit?.auto_response); + setPaymentMethods(selectedDealEdit?.payment_methods); + setRegion(selectedDealEdit?.region); + setStep(1); + } else { + setPriceType('static'); + setBuyingAsset(); + setSpendingAsset(); + setExchangeRate(); + setSpread(); + setTotalOrderAmount(); + setMinOrderValue(); + setMaxOrderValue(); + setTerms(); + setAutoResponse(); + setPaymentMethods([]); + setRegion(); + setStep(1); + } + }, [selectedDealEdit]); + + const formatAmount = (currency, amount) => { + const formattedAmount = new BigNumber(amount).decimalPlaces(4).toNumber(); + return formattedAmount; + }; + + const formatRate = (rate, spread, asset) => { + const amount = rate * (1 + Number(spread / 100 || 0)); + return formatAmount(asset, amount); + }; + + return ( +
+
+ + {dataSte.map((item, index) => ( + + ))} + +
+ +
+
+
+ + + {STRINGS['P2P.I_WANT_TO_BUY']} + + + + + + + + {STRINGS['P2P.I_WANT_TO_SELL']} + + +
+ + {selectedDealEdit && ( +
+ + {STRINGS['P2P.UPDATE_DEAL']} + +
+ )} + + {step === 1 && ( +
+
+
+
+ + {STRINGS['P2P.SELL_UPPER']} + +
+
+ +
+
+ + {STRINGS['P2P.CRYPTO_WANT_TO_SELL']} + +
+
+
+ {/* {'>'} */} +
+
+
+ + {STRINGS['P2P.RECEIVE']} + +
+
+ +
+
+ + {STRINGS['P2P.FIAT_CURRENCY_WANT_TO_RECEIVE']} + +
+
+
+
+
+
+ {/*
+ + {STRINGS['P2P.PRICE_UPPER']} + +
*/} + {/*
+ +
*/} + + {priceType === 'static' && ( + <> +
+ + {STRINGS['P2P.PRICE_UPPER']} + {' '} + {spendingAsset + ? `(${spendingAsset?.toUpperCase()})` + : ''} +
+
+ { + if (!buyingAsset) return; + setExchangeRate(e.target.value); + }} + /> +
+ + )} +
+ + {STRINGS['P2P.SPREAD_PERCENTAGE']} + +
+
+ { + setSpread(e.target.value); + }} + /> +
+
+ + {STRINGS['P2P.PRICE_PROFIT_SPREAD_SET']} + +
+
+
+ {/* {'>'} */} +
+ + {exchangeRate && ( +
+
+ + {STRINGS['P2P.UNIT_PRICE']} + +
+
+ {formatRate(exchangeRate, spread, spendingAsset)} +
+
+ + {STRINGS['P2P.PRICE_ADVERTISE_SELL']} + {' '} + {buyingAsset ? `${buyingAsset?.toUpperCase()}` : ''} +
+
+ )} +
+
+ )} + + {step === 2 && ( +
+
+
+
+ + {STRINGS['P2P.TOTAL_ASSET_SELL_1']} + {' '} + {buyingAsset?.toUpperCase()}{' '} + + {STRINGS['P2P.TOTAL_ASSET_SELL_2']} + +
+
+ { + setTotalOrderAmount(e.target.value); + }} + /> +
+ +
+ +
+ + {STRINGS['P2P.BUY_ORDER_LIMITS']} + +
+
+ + {STRINGS['P2P.MIN_MAX_ORDER_VALUE_1']} + {' '} + {spendingAsset?.toUpperCase()}{' '} + + {STRINGS['P2P.MIN_MAX_ORDER_VALUE_2']} + {' '} + {spendingAsset?.toUpperCase()} +
+
+
+
+ { + setMinOrderValue(e.target.value); + }} + /> +
+
+ {minOrderValue + ? ( + minOrderValue / + formatRate(exchangeRate, spread, spendingAsset) + ).toFixed(4) + + ' ' + + buyingAsset?.toUpperCase() + : ''}{' '} +
+
+
+
+ { + setMaxOrderValue(e.target.value); + }} + /> +
+
+ {maxOrderValue + ? ( + maxOrderValue / + formatRate(exchangeRate, spread, spendingAsset) + ).toFixed(4) + + ' ' + + buyingAsset?.toUpperCase() + : ''}{' '} +
+
+
+
+
+
+
+
+
+ + {STRINGS['P2P.PAYMENT_METHODS_SEND_FIAT']} + +
+
+ + {STRINGS['P2P.SELECT_PAYMENT_METHODS_1']} + {' '} + {p2p_config?.bank_payment_methods?.length || 0}{' '} + + {STRINGS['P2P.SELECT_PAYMENT_METHODS_2']} + {' '} + {spendingAsset?.toUpperCase()} +
+ + {p2p_config?.bank_payment_methods?.map((method) => { + return ( +
+
x.system_name === method.system_name + ) + ? 'whiteTextP2P' + : 'greyTextP2P' + } + onClick={() => { + const newSelected = [...paymentMethods]; + + if ( + newSelected.find( + (x) => x.system_name === method.system_name + ) + ) { + setPaymentMethods( + newSelected.filter( + (x) => x.system_name !== method.system_name + ) + ); + } else { + newSelected.push(method); + setPaymentMethods(newSelected); + setSelectedMethod(method); + setAddMethodDetails(true); + } + }} + > +
{method.system_name}
+ {paymentMethods?.find( + (x) => x.system_name === method.system_name + ) &&
} +
+ {paymentMethods?.find( + (x) => x.system_name === method.system_name + ) && ( +
{ + setSelectedMethod(method); + setAddMethodDetails(true); + }} + className="whiteTextP2P" + style={{ cursor: 'pointer' }} + > + + + {STRINGS['P2P.EDIT_UPPERCASE']} + + +
+ )} +
+ ); + })} +
+ +
+
+ + {STRINGS['P2P.REGION']} + +
+
+ + {STRINGS['P2P.SELECT_REGION']} + +
+ +
+
+
+ )} + + {step === 3 && ( +
+
+
+
+ + {STRINGS['P2P.TERMS']} + +
+
+ + {STRINGS['P2P.TERMS_CONDITIONS_DEAL']} + +
+ + { + setTerms(e.target.value); + }} + placeholder="Please post within 15 minutes of the deal going" + /> +
+
+
+
+
+
+ + {STRINGS['P2P.FIRST_RESPONSE']} + +
+
+ + {STRINGS['P2P.CHAT_RESPONSE']} + +
+ { + setAutoResponse(e.target.value); + }} + placeholder="Visit our website" + /> +
+
+
+ )} +
+
+ +
+ {step !== 1 && ( + + )} + +
+ + } + bodyStyle={{ + marginTop: 60, + }} + className="stake_theme" + visible={addMethodDetails} + footer={null} + onCancel={() => { + setAddMethodDetails(false); + }} + > +
+ + {STRINGS['P2P.ADD_PAYMENT_METHOD_DETAILS']} + +
+ + {selectedMethod?.fields?.map((x, index) => { + return ( +
+
{x?.name}:
+ { + if (!selectedMethod.fields[index].value) + selectedMethod.fields[index].value = ''; + + selectedMethod.fields[index].value = e.target.value; + + const newSelected = [...paymentMethods]; + + const Index = newSelected.findIndex( + (x) => x.system_name === selectedMethod.system_name + ); + + newSelected[Index].fields = selectedMethod.fields; + + setPaymentMethods(newSelected); + }} + /> +
+ ); + })} + +
+ + +
+
+
+ ); +}; + +const mapStateToProps = (state) => ({ + pairs: state.app.pairs, + coins: state.app.coins, + constants: state.app.constants, + transaction_limits: state.app.transaction_limits, + p2p_config: state.app.constants.p2p_config, +}); + +export default connect(mapStateToProps)(withConfig(P2PPostDeal)); diff --git a/web/src/containers/P2P/P2PProfile.js b/web/src/containers/P2P/P2PProfile.js new file mode 100644 index 0000000000..b37bdf72bb --- /dev/null +++ b/web/src/containers/P2P/P2PProfile.js @@ -0,0 +1,284 @@ +/* eslint-disable */ +import React, { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; + +import { IconTitle, EditWrapper } from 'components'; +import STRINGS from 'config/localizedStrings'; +import withConfig from 'components/ConfigProvider/withConfig'; +import { Button, Checkbox, message, Rate } from 'antd'; +import { fetchFeedback, fetchP2PProfile } from './actions/p2pActions'; +import { isMobile } from 'react-device-detect'; +import classnames from 'classnames'; +import moment from 'moment'; +import './_P2P.scss'; + +const P2PProfile = ({ + data, + onClose, + coins, + pairs, + constants = {}, + icons: ICONS, + transaction_limits, + tiers = {}, + user, + refresh, + setSelectedDealEdit, + setTab, + selectedProfile, + setSelectedProfile, +}) => { + const [myDeals, setMyDeals] = useState([]); + const [checks, setCheks] = useState([]); + const [myProfile, setMyProfile] = useState(); + const [selectedUser, setSelectedUser] = useState(user); + + useEffect(() => { + fetchFeedback({ merchant_id: (selectedProfile || selectedUser).id }) + .then((res) => { + setMyDeals(res.data); + }) + .catch((err) => err); + + fetchP2PProfile({ user_id: (selectedProfile || selectedUser).id }) + .then((res) => { + setMyProfile(res); + }) + .catch((err) => err); + }, [refresh, selectedProfile]); + + return ( +
+
+
+ + {STRINGS['P2P.DISPLAY_NAME']} + +
+
+ {(selectedProfile || selectedUser).full_name || ( + + {STRINGS['P2P.ANONYMOUS']} + + )} +
+ {/*
+
EMAIL
+
SMS
+
ID
+
*/} +
+
+
+
+ + {STRINGS['P2P.TOTAL_ORDERS']} + +
+
+ {myProfile?.totalTransactions} times +
+
+
+
+ + {STRINGS['P2P.COMPLETION_RATE']} + +
+
+ {(myProfile?.completionRate || 0).toFixed(2)}% +
+
+
+
+ + {STRINGS['P2P.POSITIVE_FEEDBACK']} + +
+
+ {(myProfile?.positiveFeedbackRate || 0).toFixed(2)}% +
+
+ + {STRINGS['P2P.POSITIVE']} + {' '} + {myProfile?.positiveFeedbackCount} /{' '} + + {STRINGS['P2P.NEGATIVE']} + {' '} + {myProfile?.negativeFeedbackCount} +
+
+
+
+ +
+ Feedback({myDeals.length || 0}) +
+ {myDeals.length == 0 ? ( +
+ + {STRINGS['P2P.NO_FEEDBACK']} + +
+ ) : ( + + + + + + + + + + + {myDeals.map((deal) => { + return ( + + + + + + + ); + })} + +
+ + {STRINGS['P2P.DATE']} + + + + {STRINGS['P2P.USER']} + + + + {STRINGS['P2P.COMMENT']} + + + + {STRINGS['P2P.RATING']} + +
+ {moment(deal.created_at).format('DD/MMM/YYYY, hh:mmA')} + + {deal.user.full_name || ( + + {STRINGS['P2P.ANONYMOUS']} + + )} + + {deal.comment} + + +
+ )} +
+
+ ); +}; + +const mapStateToProps = (state) => ({ + pairs: state.app.pairs, + coins: state.app.coins, + constants: state.app.constants, + transaction_limits: state.app.transaction_limits, + user: state.user, +}); + +export default connect(mapStateToProps)(withConfig(P2PProfile)); diff --git a/web/src/containers/P2P/_P2P.scss b/web/src/containers/P2P/_P2P.scss new file mode 100644 index 0000000000..200d5f0a95 --- /dev/null +++ b/web/src/containers/P2P/_P2P.scss @@ -0,0 +1,67 @@ +.P2pOrder { + .ant-steps-item-active .ant-steps-item-content { + background-color: transparent; + } + background-color: var(--base_wallet-sidebar-and-popup); + + .buySideP2P { + background-color: var(--trading_buying-related-elements); + color: var(--calculated_base_top-bar-navigation_text) !important; + } + .sellSideP2P { + background-color: var(--trading_selling-related-elements); + color: var(--calculated_base_top-bar-navigation_text) !important; + } + .subTable { + background-color: var(--base_secondary-navigation-bar); + } +} +.greenButtonP2P { + background-color: var(--specials_checks-okay-done) !important; + color: var(--calculated_base_top-bar-navigation_text) !important; + border: none; +} + +.greyButtonP2P { + background-color: var(--labels_inactive-button) !important; + color: var(--calculated_base_top-bar-navigation_text) !important; + border: none; +} +.whiteTextP2P { + color: var(--labels_important-active-labels-text-graphics) !important; +} + +.purpleTextP2P { + color: var(--specials_buttons-links-and-highlights) !important; +} + +.purpleButtonP2P { + background-color: var(--specials_buttons-links-and-highlights) !important; + color: var(--calculated_base_top-bar-navigation_text) !important; + border: none; +} +.greyTextP2P { + color: var(--labels_inactive-button); +} +.openGreyTextP2P { + color: var(--labels_secondary-inactive-label-text-graphics); +} + +.transparentButtonP2P { + background-color: transparent !important; + color: var(--labels_important-active-labels-text-graphics) !important; +} + +.postDealP2PModel { + background-color: var(--base_secondary-navigation-bar) !important; +} + +.mobile-view-p2p { + zoom: 0.5; + position: relative; +} + +.mobile-view-p2p-post { + zoom: 0.4; + position: relative; +} diff --git a/web/src/containers/P2P/actions/p2pActions.js b/web/src/containers/P2P/actions/p2pActions.js new file mode 100644 index 0000000000..f90708d834 --- /dev/null +++ b/web/src/containers/P2P/actions/p2pActions.js @@ -0,0 +1,81 @@ +import querystring from 'query-string'; +import { requestAuthenticated } from 'utils'; + +export const fetchDeals = (values) => { + const queryValues = + values && Object.keys(values).length ? querystring.stringify(values) : ''; + return requestAuthenticated(`/p2p/deal?${queryValues}`); +}; + +export const fetchTransactions = (values) => { + const queryValues = + values && Object.keys(values).length ? querystring.stringify(values) : ''; + return requestAuthenticated(`/p2p/order?${queryValues}`); +}; + +export const postDeal = (values) => { + const options = { + method: 'POST', + body: JSON.stringify(values), + }; + + return requestAuthenticated('/p2p/deal', options); +}; + +export const editDeal = (values) => { + const options = { + method: 'PUT', + body: JSON.stringify(values), + }; + + return requestAuthenticated('/p2p/deal', options); +}; + +export const createTransaction = (values) => { + const options = { + method: 'POST', + body: JSON.stringify(values), + }; + + return requestAuthenticated('/p2p/order', options); +}; + +export const updateTransaction = (values) => { + const options = { + method: 'PUT', + body: JSON.stringify(values), + }; + + return requestAuthenticated('/p2p/order', options); +}; + +export const createChatMessage = (values) => { + const options = { + method: 'POST', + body: JSON.stringify(values), + }; + + return requestAuthenticated('/p2p/order/chat', options); +}; + +export const createFeedback = (values) => { + const options = { + method: 'POST', + body: JSON.stringify(values), + }; + + return requestAuthenticated('/p2p/feedback', options); +}; + +export const fetchFeedback = (values) => { + const queryValues = + values && Object.keys(values).length ? querystring.stringify(values) : ''; + return requestAuthenticated(`/p2p/feedback?${queryValues}`); +}; + + +export const fetchP2PProfile = (values) => { + const queryValues = + values && Object.keys(values).length ? querystring.stringify(values) : ''; + return requestAuthenticated(`/p2p/profile?${queryValues}`); +}; diff --git a/web/src/containers/P2P/index.js b/web/src/containers/P2P/index.js new file mode 100644 index 0000000000..ad4750cade --- /dev/null +++ b/web/src/containers/P2P/index.js @@ -0,0 +1,207 @@ +/* eslint-disable */ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { ReactSVG } from 'react-svg'; + +import { IconTitle, EditWrapper } from 'components'; +import STRINGS from 'config/localizedStrings'; +import withConfig from 'components/ConfigProvider/withConfig'; +import { Tabs, message } from 'antd'; +import P2PDash from './P2PDash'; +import P2PMyDeals from './P2PMyDeals'; +import P2POrders from './P2POrders'; +import P2PPostDeal from './P2PPostDeal'; +import P2PProfile from './P2PProfile'; +import P2POrder from './P2POrder'; +import { fetchTransactions } from './actions/p2pActions'; +import { withRouter } from 'react-router'; +const TabPane = Tabs.TabPane; + +const P2P = ({ + data, + onClose, + coins, + pairs, + constants = {}, + icons: ICONS, + transaction_limits, + tiers = {}, + user, + router, + p2p_config, +}) => { + const [displayOrder, setDisplayOrder] = useState(false); + const [tab, setTab] = useState('0'); + const [selectedTransaction, setSelectedTransaction] = useState(); + const [refresh, setRefresh] = useState(false); + const [selectedDealEdit, setSelectedDealEdit] = useState(); + const [selectedProfile, setSelectedProfile] = useState(); + + useEffect(() => { + const arr = window.location.pathname.split('/'); + + if (arr.length === 4) { + const transId = arr[arr.length - 1]; + + fetchTransactions({ + id: transId, + }) + .then((res) => { + if (res.data.length > 0) { + setSelectedTransaction(res.data[0]); + setDisplayOrder(true); + } else { + router.push('/p2p'); + message.error(STRINGS['P2P.TRANSACTION_NOT_FOUND']); + } + }) + .catch((err) => { + router.push('/p2p'); + return err; + }); + } else setDisplayOrder(false); + + if (arr?.[2] === 'orders') { + setTab('1'); + } + if (arr?.[2] === 'profile') { + setTab('2'); + } + if (arr?.[2] === 'post-deal') { + setTab('3'); + } + if (arr?.[2] === 'mydeals') { + setTab('4'); + } + }, [window.location.pathname]); + + const changeProfileTab = (merchant) => { + setSelectedProfile(merchant); + setTab('2'); + }; + return ( +
+ {!displayOrder && ( + <> +
+ + {STRINGS['P2P.TITLE']} + +
+
+ + {STRINGS['P2P.DESCRIPTION']} + +
+ + )} + + {displayOrder && ( + + )} + {!displayOrder && ( + { + if (e !== '3') { + setSelectedDealEdit(); + } + + if (e !== '2') { + setSelectedProfile(); + } + + if (e === '0') { + router.push('/p2p'); + } else if (e === '1') { + router.push('/p2p/orders'); + } else if (e === '2') { + router.push('/p2p/profile'); + } else if (e === '3') { + router.push('/p2p/post-deal'); + } else if (e === '4') { + router.push('/p2p/mydeals'); + } + + setTab(e); + }} + > + + + + + {user?.id && + user.verification_level >= p2p_config?.starting_user_tier && ( + <> + + + + + )} + + {user?.id && ( + + + + )} + + {user?.id && + user.verification_level >= p2p_config?.starting_merchant_tier && ( + <> + + + + + + + + )} + + )} +
+ ); +}; + +const mapStateToProps = (state) => ({ + pairs: state.app.pairs, + coins: state.app.coins, + constants: state.app.constants, + transaction_limits: state.app.transaction_limits, + user: state.user, + p2p_config: state.app.constants.p2p_config, +}); + +export default connect(mapStateToProps)(withRouter(withConfig(P2P))); diff --git a/web/src/containers/Summary/MobileSummary.js b/web/src/containers/Summary/MobileSummary.js index 283f855600..5c2f551ed2 100644 --- a/web/src/containers/Summary/MobileSummary.js +++ b/web/src/containers/Summary/MobileSummary.js @@ -35,6 +35,8 @@ const MobileSummary = ({ verification_level, onStakeToken, affiliation, + onDisplayReferralList, + referral_history_config, }) => { const { fullname } = coins[BASE_CURRENCY] || DEFAULT_COIN_DATA; // const Title = STRINGS.formatString(STRINGS["SUMMARY.LEVEL_OF_ACCOUNT"],verification_level); @@ -57,6 +59,8 @@ const MobileSummary = ({ config={config} logout={logout} onInviteFriends={onInviteFriends} + onDisplayReferralList={onDisplayReferralList} + referral_history_config={referral_history_config} onUpgradeAccount={onUpgradeAccount} verification_level={verification_level} /> diff --git a/web/src/containers/Summary/_Summary.scss b/web/src/containers/Summary/_Summary.scss index 3490b8dbb8..634704178e 100644 --- a/web/src/containers/Summary/_Summary.scss +++ b/web/src/containers/Summary/_Summary.scss @@ -286,6 +286,11 @@ $trade-tab--arrow-size: 0.4rem; color: $link; font-weight: bold; } + .deposit-icon { + svg { + width: 1rem; + } + } .summary-section_1 { // height: 30rem; diff --git a/web/src/containers/Summary/components/AccountAssets.js b/web/src/containers/Summary/components/AccountAssets.js index 35c94b83f5..6460946ab9 100644 --- a/web/src/containers/Summary/components/AccountAssets.js +++ b/web/src/containers/Summary/components/AccountAssets.js @@ -1,6 +1,7 @@ import React from 'react'; import classnames from 'classnames'; import { isMobile } from 'react-device-detect'; +import { Link } from 'react-router'; import { DonutChart, Carousel, EditWrapper, NotLoggedIn } from 'components'; import STRINGS from 'config/localizedStrings'; @@ -54,10 +55,17 @@ const AccountAssets = ({ chartData = [], totalAssets, balance, coins }) => { return (
-
+
{STRINGS['SUMMARY.ACCOUNT_ASSETS_TXT_1']} + +
+ + {STRINGS['SUMMARY.MAKE_A_DEPOSIT']} + +
+
{SHOW_SUMMARY_ACCOUNT_DETAILS ? (
diff --git a/web/src/containers/Summary/components/ReferralList.js b/web/src/containers/Summary/components/ReferralList.js new file mode 100644 index 0000000000..6a2f88eb3c --- /dev/null +++ b/web/src/containers/Summary/components/ReferralList.js @@ -0,0 +1,1887 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import { Button as AntButton, Spin, Tabs, Tooltip } from 'antd'; +import { + LoadingOutlined, + CaretUpOutlined, + CaretDownOutlined, + CheckCircleOutlined, + InfoCircleOutlined, +} from '@ant-design/icons'; +import { isMobile } from 'react-device-detect'; + +import withConfig from 'components/ConfigProvider/withConfig'; +import BigNumber from 'bignumber.js'; +import moment from 'moment'; +import STRINGS from 'config/localizedStrings'; +import ICONS from 'config/icons'; +import './_ReferralList.scss'; +import { Dialog, EditWrapper, Help, IconTitle, Image } from 'components'; +import { + fetchReferralHistory, + fetchUnrealizedFeeEarnings, + postReferralCode, + fetchReferralCodes, + postSettleFees, + fetchRealizedFeeEarnings, +} from './actions'; +import { Table } from 'components'; +import { getUserReferrals } from 'actions/userAction'; +import { setSnackNotification } from 'actions/appActions'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; + +const TabPane = Tabs.TabPane; + +// const RenderDumbField = (props) => ; +const RECORD_LIMIT = 20; + +const ReferralList = ({ + affiliation_code, + affiliation, + setSnackNotification, + coins, + referral_history_config, + icons: ICON, + router, +}) => { + const [balanceHistory, setBalanceHistory] = useState([]); + const [isLoading, setIsLoading] = useState(false); + // const [currentDay, setCurrentDay] = useState(100); + // eslint-disable-next-line + const [queryValues, setQueryValues] = useState({ + // start_date: moment().subtract(currentDay, 'days').toISOString(), + // end_date: moment().subtract().toISOString(), + format: 'all', + }); + const [queryFilters, setQueryFilters] = useState({ + total: 0, + page: 1, + pageSize: 10, + limit: 50, + currentTablePage: 1, + isRemaining: true, + }); + // eslint-disable-next-line + // const [currentBalance, setCurrentBalance] = useState(); + // const [customDate, setCustomDate] = useState(false); + // const [customDateValues, setCustomDateValues] = useState(); + + // const [showReferrals, setShowReferrals] = useState(false); + const [unrealizedEarnings, setUnrealizedEarnings] = useState(0); + const [latestBalance, setLatestBalance] = useState(); + + const [displayCreateLink, setDisplayCreateLink] = useState(false); + const [displaySettle, setDisplaySettle] = useState(false); + const [linkStep, setLinkStep] = useState(0); + const [referralCode, setReferralCode] = useState(); + const [selectedOption, setSelectedOption] = useState(0); + const [referralCodes, setReferralCodes] = useState([]); + const [earningRate, setEarningRate] = useState( + referral_history_config?.earning_rate + ); + const [discount, setDiscount] = useState(0); + const [activeTab, setActiveTab] = useState('0'); + const [realizedData, setRealizedData] = useState([]); + + const handleTabChange = (key) => { + setActiveTab(key); + }; + useEffect(() => { + fetchReferralCodes() + .then((res) => { + setReferralCodes(res.data); + }) + .catch((err) => err); + + fetchRealizedFeeEarnings() + .then((res) => { + setRealizedData(res); + }) + .catch((err) => err); + fetchUnrealizedFeeEarnings() + .then((res) => { + if (res?.data?.length > 0) { + let earnings = 0; + + res.data.forEach((earning) => { + earnings += earning.accumulated_fees; + }); + + setUnrealizedEarnings( + getSourceDecimals( + referral_history_config?.currency || 'usdt', + earnings + ) + ); + } + }) + .catch((err) => err); + getUserReferrals(); + // eslint-disable-next-line + }, []); + + const HEADERS = [ + { + stringId: 'REFERRAL_LINK.TIME_OF_SETTLEMENT', + label: 'Time of settlement', + key: 'time', + renderCell: ({ created_at }, key, index) => ( + +
+ {new Date(created_at).toISOString().slice(0, 10).replace(/-/g, '/')} +
+ + ), + }, + { + stringId: 'REFERRAL_LINK.CODE', + label: STRINGS['REFERRAL_LINK.CODE'], + key: 'code', + renderCell: (data, key, index) => ( + +
+ {/* {data?.code || '-'} */} + {{data.code}}{' '} +
+ + ), + }, + + { + stringId: 'REFERRAL_LINK.EARNING', + label: `${STRINGS['REFERRAL_LINK.EARNING']} (${( + referral_history_config?.currency || 'usdt' + ).toUpperCase()})`, + key: 'earning', + className: 'd-flex justify-content-end', + renderCell: (data, key, index) => { + return ( + +
+ {data?.accumulated_fees || '-'} +
+ + ); + }, + }, + ]; + + const HEADERSREFERRAL = [ + { + stringId: 'REFERRAL_LINK.CREATION_DATE', + label: STRINGS['REFERRAL_LINK.CREATION_DATE'], + key: 'time', + renderCell: ({ created_at }, key, index) => ( + +
+ {new Date(created_at).toISOString().slice(0, 10).replace(/-/g, '/')} +
+ + ), + }, + isMobile && { + stringId: 'REFERRAL_LINK.LINK', + label: STRINGS['REFERRAL_LINK.LINK'], + key: 'link', + className: 'd-flex justify-content-center', + renderCell: (data, key, index) => { + return ( + +
+ .../signup?affiliation_code={data?.code}{' '} + { + handleCopy(); + }} + > + + + {STRINGS['REFERRAL_LINK.COPY']} + + + +
+ + ); + }, + }, + { + stringId: 'REFERRAL_LINK.CODE', + label: STRINGS['REFERRAL_LINK.CODE'], + key: 'code', + renderCell: (data, key, index) => ( + +
+ {data?.code || '-'} +
+ + ), + }, + { + stringId: 'REFERRAL_LINK.REFERRAL_COUNT', + label: STRINGS['REFERRAL_LINK.REFERRAL_COUNT'], + key: 'referral_count', + + renderCell: (data, key, index) => { + return ( + +
+ {data?.referral_count} +
+ + ); + }, + }, + { + stringId: 'REFERRAL_LINK.YOUR_EARNING_RATE', + label: STRINGS['REFERRAL_LINK.YOUR_EARNING_RATE'], + key: 'earning_rate', + + renderCell: (data, key, index) => { + return ( + +
+ {data?.earning_rate}% +
+ + ); + }, + }, + { + stringId: 'REFERRAL_LINK.DISCOUNT_GIVEN', + label: STRINGS['REFERRAL_LINK.DISCOUNT_GIVEN'], + key: 'discount', + + renderCell: (data, key, index) => { + return ( + +
+ {data?.discount}% +
+ + ); + }, + }, + !isMobile && { + stringId: 'REFERRAL_LINK.LINK', + label: STRINGS['REFERRAL_LINK.LINK'], + key: 'link', + className: 'd-flex justify-content-end', + renderCell: (data, key, index) => { + return ( + +
+ .../signup?affiliation_code={data?.code}{' '} + { + handleCopy(); + }} + > + + + {STRINGS['REFERRAL_LINK.COPY']} + + + +
+ + ); + }, + }, + ]; + + const handleCopy = () => { + setSnackNotification({ + icon: ICONS.COPY_NOTIFICATION, + content: STRINGS['COPY_SUCCESS_TEXT'], + timer: 2000, + }); + }; + + const handleSettlementNotification = () => { + setSnackNotification({ + icon: ICONS.COPY_NOTIFICATION, + content: STRINGS['REFERRAL_LINK.SETTLEMENT_SUCCESS'], + timer: 2000, + }); + }; + + const showErrorMessage = (message) => { + setSnackNotification({ + icon: ICONS.COPY_NOTIFICATION, + content: message, + timer: 2000, + }); + }; + + // const viewReferrals = (showReferrals) => { + // setShowReferrals(showReferrals); + // }; + + const handleNext = (pageCount, pageNumber) => { + const pageTemp = pageNumber % 2 === 0 ? 2 : 1; + const apiPageTemp = Math.floor((pageNumber + 1) / 2); + + if ( + RECORD_LIMIT === pageCount * pageTemp && + apiPageTemp >= affiliation.page && + affiliation.isRemaining + ) { + getUserReferrals(affiliation.page + 1, RECORD_LIMIT); + } + }; + + // const referralLink = `${process.env.REACT_APP_PUBLIC_URL}/signup?affiliation_code=${affiliation_code}`; + + const firstRender = useRef(true); + + useEffect(() => { + if (firstRender.current) { + firstRender.current = false; + } else { + setIsLoading(true); + + requestHistory(queryFilters.page, queryFilters.limit); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (firstRender.current) { + firstRender.current = false; + } else { + requestHistory(queryFilters.page, queryFilters.limit); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [queryValues]); + + const requestHistory = (page = 1, limit = 50) => { + setIsLoading(true); + fetchReferralHistory({ ...queryValues }) + .then(async (response) => { + setBalanceHistory( + page === 1 ? response.data : [...balanceHistory, ...response.data] + ); + + if (response.total) setLatestBalance(response.total); + setQueryFilters({ + total: response.count, + fetched: true, + page, + currentTablePage: page === 1 ? 1 : queryFilters.currentTablePage, + isRemaining: response.count > page * limit, + }); + + setIsLoading(false); + }) + .catch((error) => { + // const message = error.message; + setIsLoading(false); + }); + }; + + const handleSettlement = async () => { + try { + await postSettleFees(); + fetchUnrealizedFeeEarnings() + .then((res) => { + if (res?.data?.length > 0) { + let earnings = 0; + + res.data.forEach((earning) => { + earnings += earning.accumulated_fees; + }); + + setUnrealizedEarnings( + getSourceDecimals( + referral_history_config?.currency || 'usdt', + earnings + ) + ); + } + }) + .catch((err) => err); + fetchRealizedFeeEarnings() + .then((res) => { + setRealizedData(res); + }) + .catch((err) => err); + + setDisplaySettle(false); + handleSettlementNotification(); + setActiveTab('1'); + } catch (error) { + showErrorMessage(error.data.message); + } + }; + // const customDateModal = () => { + // return ( + // <> + // } + // className="stake_table_theme stake_theme" + // bodyStyle={{}} + // visible={customDate} + // width={400} + // footer={null} + // onCancel={() => { + // setCustomDate(false); + // }} + // > + //
+ //
+ //
+ // + // {STRINGS['REFERRAL_LINK.HISTORY_DESCRIPTION']} + // + //
+ //
+ //
+ // + // {STRINGS['REFERRAL_LINK.START_DATE']} + // + //
+ // { + // setCustomDateValues({ + // ...customDateValues, + // start_date: dateString, + // }); + // }} + // format={'YYYY/MM/DD'} + // /> + //
+ //
+ //
+ // + // {STRINGS['REFERRAL_LINK.END_DATE']} + // + //
+ // { + // setCustomDateValues({ + // ...customDateValues, + // end_date: dateString, + // }); + // }} + // format={'YYYY/MM/DD'} + // /> + //
+ //
+ //
+ //
+ // { + // setCustomDate(false); + // }} + // style={{ + // backgroundColor: '#5D63FF', + // color: 'white', + // flex: 1, + // height: 35, + // }} + // type="default" + // > + // + // {STRINGS['REFERRAL_LINK.BACK']} + // + // + // { + // try { + // if (!customDateValues.end_date) { + // message.error('Please choose an end date'); + // return; + // } + // if (!customDateValues.start_date) { + // message.error('Please choose a start date'); + // return; + // } + // const duration = moment.duration( + // moment(customDateValues.end_date).diff( + // moment(customDateValues.start_date) + // ) + // ); + // const months = duration.asMonths(); + + // if (months > 3) { + // message.error( + // 'Date difference cannot go further than 3 months' + // ); + // return; + // } + + // setCurrentDay(90); + // setQueryValues({ + // start_date: moment(customDateValues.start_date) + // .startOf('day') + // .toISOString(), + // end_date: moment(customDateValues.end_date) + // .endOf('day') + // .toISOString(), + // }); + // setCustomDate(false); + // } catch (error) { + // console.log({ error }); + // message.error('Something went wrong'); + // } + // }} + // style={{ + // backgroundColor: '#5D63FF', + // color: 'white', + // flex: 1, + // height: 35, + // }} + // type="default" + // > + // + // {STRINGS['REFERRAL_LINK.PROCEED']} + // + // + //
+ //
+ // + // ); + // }; + const generateUniqueCode = () => { + const characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let code = ''; + + for (let i = 0; i < 6; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + code += characters[randomIndex]; + } + + return code; + }; + + const onHandleClose = () => { + setDisplayCreateLink(false); + setLinkStep(0); + }; + + const createReferralCode = () => { + return ( +
+ onHandleClose()} + > + {linkStep === 0 && ( + <> +
+
+
+ +
+ + {STRINGS['REFERRAL_LINK.CREATE_REFERRAL_LINK']} + +
+
+
+ + {STRINGS['REFERRAL_LINK.CREATE_UNIQUE_REFERRAL']} + +
+
+ + {STRINGS['REFERRAL_LINK.REFERRAL_CODE']} + +
+
{referralCode}
+ {/* { + if (e.target.value.length <= 6) + setReferralCode(e.target.value?.toUpperCase()); + }} + /> */} +
+ + {STRINGS['REFERRAL_LINK.EXAMPLE']} + +
+
+ {process.env.REACT_APP_PUBLIC_URL}/signup?affiliation_code= + {referralCode} +
+
+
+ { + setDisplayCreateLink(false); + }} + className="back-btn" + > + + {STRINGS['REFERRAL_LINK.BACK']} + + + { + if (referralCode.length === 0) { + showErrorMessage( + STRINGS['REFERRAL_LINK.REFERRAL_CODE_ZERO'] + ); + return; + } + setLinkStep(1); + }} + className="next-btn" + type="default" + > + + {STRINGS['REFERRAL_LINK.NEXT']} + + +
+
+ + )} + {linkStep === 1 && ( + <> +
+
+
+ +
+ + {STRINGS['REFERRAL_LINK.EARNING_DISCOUNT']} + +
+
+
+ + {STRINGS['REFERRAL_LINK.DESCRIPTION']} + +
+
+
+ + {STRINGS['REFERRAL_LINK.DISCOUNT_RATION']} + +
+ + + +
+
+ {isMobile ? ( +
+
{ + setSelectedOption(0); + }} + className="eraning-rate-mobile-field" + > +
+ + {STRINGS['REFERRAL_LINK.YOUR_EARNING_RATE']} + +
+
+
{earningRate}%
+
+
+
+
{ + setSelectedOption(1); + }} + className="discount-mobile-field" + > +
+ + { + STRINGS[ + 'REFERRAL_LINK.DISCOUNT_GIVEN_TO_FRIEND' + ] + } + +
+
+
{discount}%
+
+
+
+ ) : ( + <> +
{ + setSelectedOption(0); + }} + className="eraning-rate-field" + > +
+ + {STRINGS['REFERRAL_LINK.YOUR_EARNING_RATE']} + +
+
+
{earningRate}%
+
+
+
:
+
{ + setSelectedOption(1); + }} + className="discount-field" + > +
+ + { + STRINGS[ + 'REFERRAL_LINK.DISCOUNT_GIVEN_TO_FRIEND' + ] + } + +
+
+
{discount}%
+
+
+ + )} +
+
{ + e.stopPropagation(); + if (selectedOption === 0) { + if ( + earningRate >= 0 && + earningRate <= + referral_history_config.earning_rate + ) { + let newDiscount = discount; + if (discount >= 10) { + newDiscount -= 10; + setDiscount(newDiscount); + } + if ( + earningRate + newDiscount < + referral_history_config.earning_rate + ) + setEarningRate(earningRate + 10); + } + } else { + if ( + discount >= 0 && + discount <= referral_history_config.earning_rate + ) { + let newEarningRate = earningRate; + if (earningRate >= 10) { + newEarningRate -= 10; + if (newEarningRate === 0) return; + setEarningRate(newEarningRate); + } + if ( + newEarningRate + discount < + referral_history_config.earning_rate + ) + setDiscount(discount + 10); + } + } + }} + > + +
+
{ + e.stopPropagation(); + if (selectedOption === 0) { + if ( + earningRate > 0 && + earningRate <= + referral_history_config.earning_rate + ) { + const newEarningRate = earningRate - 10; + if (newEarningRate === 0) return; + if (earningRate >= 10) + setEarningRate(newEarningRate); + if ( + newEarningRate + discount < + referral_history_config.earning_rate + ) + setDiscount(discount + 10); + } + } else { + if ( + discount > 0 && + discount <= referral_history_config.earning_rate + ) { + const newDiscount = discount - 10; + if (discount >= 10) setDiscount(newDiscount); + if ( + earningRate + newDiscount < + referral_history_config.earning_rate + ) + setEarningRate(earningRate + 10); + } + } + }} + > + +
+
+
+
+
+ { + setLinkStep(0); + }} + className="back-btn" + type="default" + > + + {STRINGS['REFERRAL_LINK.BACK']} + + + { + setLinkStep(2); + }} + className="next-btn" + type="default" + > + + {STRINGS['REFERRAL_LINK.NEXT']} + + +
+
+ + )} + + {linkStep === 2 && ( + <> +
+
+
+ + {STRINGS['REFERRAL_LINK.REVIEW_AND_CONFIRM']} + +
+
+ + {STRINGS['REFERRAL_LINK.PLEASE_CHECK_BELOW']} + +
+
+ + {STRINGS['REFERRAL_LINK.DISCOUNT_RATIO']} + +
+
+
+
+
+ + {STRINGS['REFERRAL_LINK.REFERRAL_CODE']} + +
+
{referralCode}
+
+ +
+
+ + {STRINGS['REFERRAL_LINK.YOUR_EARNING_RATE']} + +
+
{earningRate}%
+
+
+
+ + {STRINGS['REFERRAL_LINK.DISCOUNT_GIVEN']} + +
+
{discount}%
+
+
+
+ +
+
+ + {STRINGS['REFERRAL_LINK.EXAMPLE']} + +
+
+ {process.env.REACT_APP_PUBLIC_URL} + /signup?affiliation_code={' '} + + {referralCode} + +
+
+
+
+ { + setLinkStep(1); + }} + className="back-btn" + type="default" + > + + {STRINGS['REFERRAL_LINK.BACK']} + + + { + try { + if (referralCodes?.data?.length < 3) { + await postReferralCode({ + earning_rate: earningRate, + discount, + code: referralCode, + }); + fetchReferralCodes() + .then((res) => { + setReferralCodes(res.data); + }) + .catch((err) => err); + } + setLinkStep(3); + } catch (error) { + showErrorMessage(error.data.message); + } + }} + className="next-btn" + type="default" + > + + {STRINGS['REFERRAL_LINK.CONFIRM']} + + +
+
+ + )} + + {linkStep === 3 && ( + <> +
+
+
+ {!isMobile && ( +
+ +
+ )} +
+ + {STRINGS['REFERRAL_LINK.LINK_CREATED']} + +
+
+
+ + {STRINGS['REFERRAL_LINK.DESCRIPTION_2']} + +
+ +
+ + {STRINGS['REFERRAL_LINK.REFERRAL_LINK']} + +
+
+
+ {process.env.REACT_APP_PUBLIC_URL} + /signup?affiliation_code={referralCode} +
+ { + handleCopy(); + }} + > +
+ + {STRINGS['REFERRAL_LINK.COPY']} + +
+
+
+
+
+ { + setDisplayCreateLink(false); + setLinkStep(0); + setReferralCode(); + setDiscount(0); + setEarningRate(referral_history_config?.earning_rate); + }} + className="okay-btn" + type="default" + > + + {STRINGS['REFERRAL_LINK.OKAY']} + + +
+
+ + )} +
+
+ ); + }; + + const settleReferral = () => { + return unrealizedEarnings < 1 ? ( + setDisplaySettle(false)} + > +
+
+
+
+ +
+
+ + {STRINGS['REFERRAL_LINK.INSUFFICIENT_LABEL']} + +
+
+ + + {STRINGS.formatString( + STRINGS['REFERRAL_LINK.INSUFFICIENT_INFO_1'], + + {STRINGS['REFERRAL_LINK.INSUFFICIENT_INFO_2']} + , + + {( + referral_history_config?.currency || 'usdt' + ).toUpperCase()} + + )} + + +
+
+ { + setDisplaySettle(false); + }} + className="okay-btn" + type="default" + > + + {STRINGS['REFERRAL_LINK.BACK']} + + +
+
+
+ ) : ( + setDisplaySettle(false)} + > +
+
+
+
+ +
+
+ + {STRINGS['REFERRAL_LINK.EARNING_SETTLEMENT']} + +
+
+
+ Amount to settle:{' '} + {unrealizedEarnings}{' '} + {(referral_history_config?.currency || 'usdt').toUpperCase()} +
+ +
+ + {STRINGS['REFERRAL_LINK.DO_YOU_WANT_TO_SETTLE']} + +
+
+
+ { + setDisplaySettle(false); + }} + className="back-btn" + type="default" + > + + {STRINGS['REFERRAL_LINK.BACK']} + + + { + handleSettlement(); + }} + className="next-btn" + type="default" + > + + {STRINGS['REFERRAL_LINK.SETTLE']} + + +
+
+
+ ); + }; + + const getSourceDecimals = (symbol, value) => { + const incrementUnit = coins[symbol].increment_unit; + const decimalPoint = new BigNumber(incrementUnit).dp(); + const sourceAmount = new BigNumber(value || 0) + .decimalPlaces(decimalPoint) + .toNumber(); + + return sourceAmount; + }; + return ( +
+ +
+ +
+ {!isMobile && ( +
+ + {STRINGS['REFERRAL_LINK.REFERRAL_INFO']} + +
+ )} + {displayCreateLink && createReferralCode()} + {displaySettle && settleReferral()} + {!isMobile && ( +
+ router.push('/summary')} + > + {'<'} + + {STRINGS['REFERRAL_LINK.BACK_LOWER']} + + + + + {STRINGS['REFERRAL_LINK.BACK_TO_SUMMARY']} + + +
+ )} + + +
+
+
+
+
+ +
+ {!isMobile && ( +
+ + {STRINGS['REFERRAL_LINK.EARNINGS']} + +
+ )} +
+
+ {isMobile && ( +
+ + {STRINGS['REFERRAL_LINK.EARNINGS']} + +
+ )} +
+ + {STRINGS['REFERRAL_LINK.EARNING_DESC']} + +
+ { + handleTabChange('1'); + }} + > + + + {STRINGS['REFERRAL_LINK.VIEW_HISTORY']} + + + + {/*
+ + {STRINGS['REFERRAL_LINK.DATA_COLLECTED']} + {' '} + {moment(referral_history_config?.date_enabled).format( + 'YYYY/MM/DD' + )} + . +
*/} + {isMobile && ( +
+
+
+ + + {STRINGS['REFERRAL_LINK.EARNT']} + + {' '} +
+ {getSourceDecimals( + referral_history_config?.currency || 'usdt', + latestBalance + )}{' '} + {( + referral_history_config?.currency || 'usdt' + ).toUpperCase()}{' '} +
+
+
+
+ + {STRINGS['REFERRAL_LINK.UNSETTLED']} + {' '} + {unrealizedEarnings}{' '} + {( + referral_history_config?.currency || 'usdt' + ).toUpperCase()} + + + + + +
+
+
+ { + setDisplaySettle(true); + }} + size="small" + disabled={ + unrealizedEarnings === 0 || + unrealizedEarnings < + referral_history_config?.minimum_amount + } + className="settle-btn" + > + + {STRINGS['REFERRAL_LINK.SETTLE']} + + +
+
+
+ )} +
+
+ {!isMobile && ( +
+
+
+ + + {STRINGS['REFERRAL_LINK.EARNT']} + + {' '} +
+ {getSourceDecimals( + referral_history_config?.currency || 'usdt', + latestBalance + )}{' '} + {( + referral_history_config?.currency || 'usdt' + ).toUpperCase()}{' '} +
+
+
+
+ + {STRINGS['REFERRAL_LINK.UNSETTLED']} + {' '} + {unrealizedEarnings}{' '} + {( + referral_history_config?.currency || 'usdt' + ).toUpperCase()} + + + + + +
+
+
+ { + setDisplaySettle(true); + }} + size="small" + disabled={ + unrealizedEarnings === 0 || + unrealizedEarnings < + referral_history_config?.minimum_amount + } + className="settle-btn" + > + + {STRINGS['REFERRAL_LINK.SETTLE']} + + +
+
+
+ )} +
+
+
+
+
+ + {!isMobile && ( +
+
+ + {STRINGS['REFERRAL_LINK.INVITE_LINKS']} + +
+ {referralCodes?.data?.length < 3 && ( + { + if (!referralCode) { + try { + const code = generateUniqueCode(); + setReferralCode(code); + setDisplayCreateLink(true); + } catch (error) { + showErrorMessage(error.data.message); + } + } else setDisplayCreateLink(true); + }} + > + + + {STRINGS['REFERRAL_LINK.CREATE_LINK']} + + + + )} +
+ + {STRINGS['REFERRAL_LINK.REFER_DESC']} + +
+
+ )} +
+ {isMobile && ( +
+
+ + {STRINGS['REFERRAL_LINK.INVITE_LINKS']} + +
+ {referralCodes?.data?.length < 3 && ( + { + if (!referralCode) { + try { + const code = generateUniqueCode(); + setReferralCode(code); + setDisplayCreateLink(true); + } catch (error) { + showErrorMessage(error.data.message); + } + } else setDisplayCreateLink(true); + }} + > + + + {STRINGS['REFERRAL_LINK.CREATE_LINK']} + + + + )} +
+ + {STRINGS['REFERRAL_LINK.REFER_DESC']} + +
+
+ )} +
+ + {referralCodes?.data?.length === 0 && ( +
+
+ +
+
+ + {STRINGS['REFERRAL_LINK.NO_LINK']} + +
+ {referralCodes?.data?.length < 3 && ( +
{ + if (!referralCode) { + try { + const code = generateUniqueCode(); + setReferralCode(code); + setDisplayCreateLink(true); + } catch (error) { + showErrorMessage(error.data.message); + } + } else setDisplayCreateLink(true); + }} + > + + + {STRINGS['REFERRAL_LINK.CREATE_LINK']} + + +
+ )} +
+ )} + + {referralCodes?.data?.length > 0 && ( +
+ + + )} + + + +
+
+
+
+
+
+ +
+ {!isMobile && ( +
+ + {STRINGS['REFERRAL_LINK.ALL_EVENTS']} + +
+ )} +
+
+ {isMobile && ( +
+ + {STRINGS['REFERRAL_LINK.ALL_EVENTS']} + +
+ )} +
+ + {STRINGS['REFERRAL_LINK.EVENTS_DESC']} + +
+ {isMobile && ( +
+ )} +
+ + {STRINGS.formatString( + STRINGS['REFERRAL_LINK.DATA_COLLECTION'], + moment( + referral_history_config?.date_enabled + ).format('YYYY/MM/DD') + )} + +
+ {/*
+ + {STRINGS['REFERRAL_LINK.DATA_DESC']} + {' '} + { + if ( + unrealizedEarnings > 0 && + unrealizedEarnings > + referral_history_config?.minimum_amount + ) + setDisplaySettle(true); + }} + style={{ + color: '#4E54BE', + cursor: 'pointer', + fontWeight: 'bold', + textDecoration: 'underline', + }} + > + + {STRINGS['REFERRAL_LINK.SETTLE_HERE']} + + +
*/} +
+ + + {STRINGS['REFERRAL_LINK.EARNT']} + + {' '} + + {getSourceDecimals( + referral_history_config?.currency || 'usdt', + latestBalance + )}{' '} + {( + referral_history_config?.currency || 'usdt' + ).toUpperCase()}{' '} + +
+
+
+
+
+
+
+ {!isMobile && ( +
+ +
+ )} +
+ {/*
+ { + setCurrentDay(100); + setQueryValues({ + format: 'all', + }); + }} + > + + {STRINGS['REFERRAL_LINK.ALL']} + + + { + setCurrentDay(7); + setQueryValues({ + start_date: moment().subtract(7, 'days').toISOString(), + end_date: moment().subtract().toISOString(), + }); + }} + > + 1 + + {STRINGS['REFERRAL_LINK.WEEK']} + + + { + setCurrentDay(30); + setQueryValues({ + start_date: moment().subtract(30, 'days').toISOString(), + end_date: moment().subtract().toISOString(), + }); + }} + > + 1 {' '} + + {STRINGS['REFERRAL_LINK.MONTH']} + + + { + setCurrentDay(90); + setQueryValues({ + start_date: moment().subtract(90, 'days').toISOString(), + end_date: moment().subtract().toISOString(), + }); + }} + > + 3 {' '} + + {STRINGS['REFERRAL_LINK.MONTHS']} + + + { + setCustomDate(true); + }} + > + + {STRINGS['REFERRAL_LINK.CUSTOM']} + + +
*/} +
+
+ {realizedData.loading && ( +
+ +
+ )} + + + + + + + ); +}; + +const mapStateToProps = (state) => ({ + coins: state.app.coins, + balances: state.user.balance, + pricesInNative: state.asset.oraclePrices, + dust: state.app.constants.dust, + referral_history_config: state.app.constants.referral_history_config, + affiliation: state.user.affiliation || {}, + is_hap: state.user.is_hap, +}); + +const mapDispatchToProps = (dispatch) => ({ + getUserReferrals: bindActionCreators(getUserReferrals, dispatch), + setSnackNotification: bindActionCreators(setSnackNotification, dispatch), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(withConfig(ReferralList)); diff --git a/web/src/containers/Summary/components/TraderAccounts.js b/web/src/containers/Summary/components/TraderAccounts.js index 6baf75493f..19789d8ea0 100644 --- a/web/src/containers/Summary/components/TraderAccounts.js +++ b/web/src/containers/Summary/components/TraderAccounts.js @@ -16,8 +16,10 @@ const TraderAccounts = ({ onUpgradeAccount, logout, onInviteFriends, + onDisplayReferralList, verification_level, selectedAccount, + referral_history_config, icons: ICONS, }) => { const level = selectedAccount @@ -84,8 +86,8 @@ const TraderAccounts = ({ {description} - {!isAccountDetails && user.discount ? ( -
+ {user.discount > 0 ? ( +
(
- + {children}
@@ -148,6 +157,29 @@ const TraderAccounts = ({ )} + + {isLoggedIn() && ( + +
+
+ +
+ + {STRINGS.formatString(STRINGS['SUMMARY.WALLET_FUNDING'])} + +
+ +
+ + {STRINGS['SUMMARY.MAKE_A_DEPOSIT']} + +
+ +
+ )} )} {isAccountDetails && ( diff --git a/web/src/containers/Summary/components/_ReferralList.scss b/web/src/containers/Summary/components/_ReferralList.scss new file mode 100644 index 0000000000..df099d01a6 --- /dev/null +++ b/web/src/containers/Summary/components/_ReferralList.scss @@ -0,0 +1,608 @@ +.referralLabel { + color: var(--labels_important-active-labels-text-graphics); +} + +.highChartColor .highcharts-graph { + stroke: var(--labels_important-active-labels-text-graphics); +} + +.highChartColor .highcharts-point { + stroke: var(--labels_important-active-labels-text-graphics); + fill: var(--labels_important-active-labels-text-graphics); +} + +.referral-list-wrapper { + width: 80%; + margin-left: auto; + margin-right: auto; +} + +.fs-13 { + font-size: 13px; +} + +.fs-14 { + font-size: 14px; +} + +.summary-block_wrapper { + background-color: var(--base_wallet-sidebar-and-popup); + .referral-table-wrapper { + .history-table-link { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 3%; + color: var(--specials_buttons-links-and-highlights); + } + .referral-copy-link { + color: var(--labels_important-active-labels-text-graphics); + padding: 5px; + cursor: pointer; + background-color: var(--specials_buttons-links-and-highlights); + border-radius: 10px; + } + .summary-table-link { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 3%; + } + } +} + +.referral-active-text { + color: var(--labels_important-active-labels-text-graphics); +} + +.referral-inActive-text { + color: var(--labels_secondary-inactive-label-text-graphics); +} + +.referral-header-icon-wrapper { + .icon_title-wrapper { + display: flex; + justify-content: flex-start; + flex-direction: row; + + .icon_title-text.title { + font-size: 3rem !important; + padding-top: 2.25rem !important; + } + } +} + +.referral_table_theme { + .ant-modal-body { + background-color: var(--base_wallet-sidebar-and-popup) !important; + } + + .ReactModal__Content { + width: 40rem; + } +} + +.back-label { + cursor: pointer; + text-decoration: underline; + color: var(--specials_buttons-links-and-highlights); +} + +.summary-referral-container { + padding: 2%; + + .summary-referral-wrapper { + display: flex; + justify-content: space-between; + + .settle-link, + .view-history-label { + color: var(--specials_buttons-links-and-highlights); + text-decoration: underline; + cursor: pointer; + font-weight: bold; + } + + .earning-icon-wrapper { + display: flex; + + .refer-icon { + fill: var(--labels_important-active-labels-text-graphics); + } + + .margin-aligner { + margin-bottom: 1% !important; + } + + .earning-label { + font-size: 18px; + font-weight: bold; + } + } + + .earn-info-wrapper { + margin-right: 3%; + + .earn-info-border { + border-bottom: 1px solid + var(--labels_important-active-labels-text-graphics); + } + + .earn-text-wrapper { + display: flex; + justify-content: center; + + .field-label { + margin-left: 2%; + } + } + + .settle-btn { + background-color: var(--specials_buttons-links-and-highlights); + color: var(--labels_important-active-labels-text-graphics); + border: none; + } + } + } +} + +.history-referral-container { + padding: 2%; + .summary-history-referral { + display: flex; + justify-content: space-between !important; + } + + .history-referral-wrapper { + .earning-icon-wrapper { + display: flex; + + .refer-icon { + fill: var(--labels_important-active-labels-text-graphics); + } + + .margin-aligner { + margin-bottom: 1% !important; + } + .earning-label { + font-size: 18px; + font-weight: bold; + } + } + } + + .history-refer-icon { + .margin-aligner { + opacity: 0.5; + margin-bottom: unset; + margin-right: unset; + + svg { + width: 40% !important; + height: 70% !important; + } + } + .margin-aligner div { + display: flex; + justify-content: flex-end; + } + } +} + +.new-refer-wrapper { + background-color: var(--calculated_quick_trade-bg) !important; + color: var(--labels_important-active-labels-text-graphics); + padding: 15px; + + .new-refer-icon-wrapper { + display: flex; + + .margin-aligner { + margin-bottom: 1% !important; + } + } + + .create-link-label { + color: var(--specials_buttons-links-and-highlights); + text-decoration: underline; + font-weight: bold; + margin-top: 0.5%; + cursor: pointer; + } + + .content-field { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 500px; + } + + .no-link-icon { + .margin-aligner { + margin-bottom: 5%; + margin-right: unset; + + svg { + width: 100% !important; + height: 100% !important; + } + } + } +} + +.settle-popup-wrapper { + color: var(--labels_important-active-labels-text-graphics); + + .earning-icon-wrapper { + display: flex; + align-items: center; + .insufficient-icon { + text-align: center; + + svg { + width: 5rem !important; + height: 5rem !important; + } + } + .margin-aligner { + svg { + width: 4rem !important; + height: 4rem !important; + } + } + + .refer-icon { + fill: var(--labels_important-active-labels-text-graphics); + } + + .margin-aligner { + margin-bottom: 1% !important; + } + + .earning-label { + font-size: 18px; + font-weight: bold; + } + } +} + +.refer-code-popup-wrapper, +.referral-popup-earning-wrapper, +.referral-final-popup { + color: var(--labels_important-active-labels-text-graphics); + + .new-referral-wrapper { + .new-referral-label { + font-size: 16px; + margin: 2% 0 3% 2%; + } + } + + .custom-input-field { + padding: 2%; + width: 100%; + border: 1px solid var(--specials_buttons-links-and-highlights); + } +} + +.referral-popup-earning-wrapper { + .discount-label { + margin: 5% 0; + font-weight: bold; + } + + .discount-tooltip { + padding-top: 4%; + } + + .discount-tooltip-mobile { + padding-top: 3%; + } + + .earn-field-wrapper { + width: 100%; + border: 1px solid var(--labels_important-active-labels-text-graphics); + border-radius: 10px; + display: flex; + flex-direction: row; + justify-content: space-between; + + .earn-info-border { + border-bottom: 1px solid + var(--labels_important-active-labels-text-graphics); + } + + .eraning-rate-field, + .discount-field, + .eraning-rate-mobile-field, + .discount-mobile-field { + font-size: 12px; + cursor: pointer; + padding: 2%; + } + + .eraning-rate-mobile-field, + .discount-mobile-field { + padding: 5% !important; + } + } + + .earn-border-left { + border-left: 1px solid var(--labels_important-active-labels-text-graphics); + padding-top: 10% !important; + padding-left: 1% !important; + + .caret-icon-mobile { + svg { + width: 5rem !important; + height: 5rem !important; + } + } + + .caret-down-icon-mobile { + svg { + height: 8rem !important; + } + } + } + + .caret-icon-wrapper { + width: 15%; + text-align: center; + padding-left: 3%; + border-bottom-right-radius: 10px; + border-top-right-radius: 10px; + cursor: pointer; + display: flex; + flex-direction: column; + background-color: var(--base_background); + + .caret-up-icon, + .caret-up-icon-mobile { + width: 0; + height: 40%; + line-height: 0; + } + + .caret-up-icon-mobile { + height: 15% !important; + } + + .caret-down-icon { + width: 0; + height: 0; + line-height: 0; + } + + .caret-icon { + svg { + width: 3rem !important; + height: 3rem !important; + } + } + } +} + +.confirm-fild-wrapper { + color: var(--labels_important-active-labels-text-graphics); + + .confirm-field-header { + font-weight: bold; + color: var(--labels_important-active-labels-text-graphics); + font-size: 18px; + margin-bottom: 1%; + } + + .discount-field { + margin: 2% 0 1% 0; + font-weight: bold; + } + + .referral-field-content-wrapper { + padding: 2%; + width: 100%; + border: 1px solid var(--labels_important-active-labels-text-graphics); + + .referral-field-content { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 10%; + } + + .earn-rate-content, + .discount-content { + display: flex; + flex-direction: row; + justify-content: space-between; + } + } +} + +.referral-final-popup { + .checked-icon { + padding-top: 2%; + + svg { + width: 2rem; + height: 2rem; + } + } + + .desc-label { + margin-bottom: 2%; + } + + .custom-input { + padding: 2%; + width: 100%; + border: 1px solid var(--labels_important-active-labels-text-graphics); + color: var(--specials_buttons-links-and-highlights); + display: flex; + justify-content: space-between; + align-items: center; + + .link-content { + text-decoration: underline; + cursor: pointer; + } + } +} + +.referral-popup-btn-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 5%; + + .back-btn, + .next-btn, + .okay-btn { + background-color: var(--specials_buttons-links-and-highlights); + width: 45%; + color: var(--labels_important-active-labels-text-graphics); + border: none; + } + + .okay-btn { + width: 100% !important; + } +} + +.layout-mobile { + .referral-list-wrapper { + .earning-discount-label { + font-size: 18px; + } + width: 100%; + .ant-tabs-nav-list { + margin-left: 2rem; + } + .referral-popup { + display: flex !important; + flex-direction: column !important; + justify-content: space-between !important; + } + .refer-code-popup-wrapper, + .referral-popup-earning-wrapper, + .referral-final-popup { + .unique-referral { + margin-bottom: 15%; + } + + .referral-label-mobile { + font-size: 14px; + } + } + .history-referral-container { + .referral-history-content { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + } + + .events-desc-border { + width: 14%; + border-bottom: 1px solid + var(--labels_secondary-inactive-label-text-graphics); + } + } + + .settle-pop-up { + .settle { + display: flex !important; + flex-direction: column !important; + justify-content: space-between !important; + } + .settle-popup-wrapper { + margin-bottom: 10%; + + .referral-earning-settlement { + font-size: 18px; + } + } + } + } + + .summary-block_wrapper { + .create-referral-link { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + gap: 2rem; + } + .new-refer-mobile-wrapper { + padding: 2rem; + } + + .referral-active-wrapper { + display: flex; + gap: 1rem; + padding: 1rem; + } + .referral-active-text { + .earn-info-wrapper { + width: 75% !important; + + .earn-text-wrapper { + justify-content: flex-start; + padding: 5% 0; + } + } + } + .referral-table-wrapper { + .table-wrapper th, + tr { + font-size: 12px; + } + + .table-wrapper th { + text-wrap: nowrap; + } + + td { + padding: 1rem !important; + } + + .table_container { + .table-content { + overflow-x: scroll; + } + } + } + } + + .content-field { + min-height: 20rem !important; + } +} + +@media screen and (max-width: 550px) { + .layout-mobile { + .icon_title-wrapper { + margin-left: 2rem; + } + .summary-block_wrapper { + .referral-table-wrapper { + .table-wrapper th { + padding: 1rem; + } + } + } + } +} + +@media screen and (max-width: 450px) { + .layout-mobile { + .summary-block_wrapper { + .referral-table-wrapper { + .table-wrapper th { + font-size: 12px; + } + } + } + } +} diff --git a/web/src/containers/Summary/components/actions.js b/web/src/containers/Summary/components/actions.js new file mode 100644 index 0000000000..bef00db76d --- /dev/null +++ b/web/src/containers/Summary/components/actions.js @@ -0,0 +1,41 @@ +import querystring from 'query-string'; +import { requestAuthenticated } from 'utils'; + +export const fetchReferralHistory = (values) => { + const queryValues = + values && Object.keys(values).length ? querystring.stringify(values) : ''; + return requestAuthenticated(`/user/referral/history?${queryValues}`); +}; + +export const fetchUnrealizedFeeEarnings = () => { + return requestAuthenticated(`/user/referral/unrealized`); +}; + +export const fetchRealizedFeeEarnings = (values) => { + const queryValues = + values && Object.keys(values).length ? querystring.stringify(values) : ''; + return requestAuthenticated(`/user/referral/realized?${queryValues}`); +}; + +export const fetchReferralCodes = () => { + return requestAuthenticated(`/user/referral/code`); +}; + +export const generateReferralCode = () => { + return requestAuthenticated(`/user/referral/generate`); +}; + +export const postSettleFees = () => { + const options = { + method: 'POST', + }; + return requestAuthenticated(`/user/referral/unrealized`, options); +}; + +export const postReferralCode = (values) => { + const options = { + method: 'POST', + body: JSON.stringify(values), + }; + return requestAuthenticated(`/user/referral/code`, options); +}; diff --git a/web/src/containers/Summary/index.js b/web/src/containers/Summary/index.js index 325e5c6a44..0e9742e0d5 100644 --- a/web/src/containers/Summary/index.js +++ b/web/src/containers/Summary/index.js @@ -40,6 +40,8 @@ class Summary extends Component { selectedAccount: '', currentTradingAccount: this.props.verification_level, lastMonthVolume: 0, + displaySummary: true, + displayReferralList: false, }; componentDidMount() { @@ -59,6 +61,16 @@ class Summary extends Component { ); this.setState({ lastMonthVolume }); } + + if (this.state.displayReferralList) { + this.props.router.push('/referral'); + } + } + + componentDidUpdate() { + if (this.state.displayReferralList) { + this.props.router.push('/referral'); + } } UNSAFE_componentWillReceiveProps(nextProps) { @@ -120,6 +132,14 @@ class Summary extends Component { }); }; + onDisplayReferralList = () => { + this.setState({ displayReferralList: true, displaySummary: false }); + }; + + goBackReferral = () => { + this.setState({ displayReferralList: false, displaySummary: true }); + }; + onStakeToken = () => { this.props.setNotification(NOTIFICATIONS.STAKE_TOKEN); }; @@ -137,6 +157,7 @@ class Summary extends Component { totalAsset, router, icons: ICONS, + referral_history_config, } = this.props; const { selectedAccount, @@ -169,10 +190,11 @@ class Summary extends Component { STRINGS['SUMMARY.LEVEL_OF_ACCOUNT'], verification_level ); + return (
- {!isMobile && ( + {!isMobile && !this.state.displayReferralList && ( )} - {isMobile ? ( + {isMobile && !this.state.displayReferralList && ( - ) : ( + )} + {this.state.displaySummary && !isMobile && (
@@ -220,27 +245,31 @@ class Summary extends Component { onUpgradeAccount={this.onUpgradeAccount} onInviteFriends={this.onInviteFriends} verification_level={verification_level} + referral_history_config={ + this.props.referral_history_config + } + onDisplayReferralList={this.onDisplayReferralList} />
{/* - - */} + title={STRINGS["SUMMARY.TASKS"]} + wrapperClassname="w-100" + > + + */} {/*
*/} + className={classnames( + 'assets-wrapper', + 'asset_wrapper_width' + )} + > */} {/*
- - // - // {` ${formatAverage(formatBaseAmount(lastMonthVolume))}`} - // - // {` ${fullname} ${STRINGS.formatString(STRINGS["SUMMARY.NOMINAL_TRADING_WITH_MONTH"], moment().subtract(1, "month").startOf("month").format('MMMM')).join('')}`} - // - // } - > - -
*/} + + // + // {` ${formatAverage(formatBaseAmount(lastMonthVolume))}`} + // + // {` ${fullname} ${STRINGS.formatString(STRINGS["SUMMARY.NOMINAL_TRADING_WITH_MONTH"], moment().subtract(1, "month").startOf("month").format('MMMM')).join('')}`} + // + // } + > + +
*/}
({ constants: state.app.constants, chartData: state.asset.chartData, totalAsset: state.asset.totalAsset, + referral_history_config: state.app.constants.referral_history_config, }); const mapDispatchToProps = (dispatch) => ({ diff --git a/web/src/containers/Trade/components/ActiveOrders.js b/web/src/containers/Trade/components/ActiveOrders.js index 262849c23a..aa42460e63 100644 --- a/web/src/containers/Trade/components/ActiveOrders.js +++ b/web/src/containers/Trade/components/ActiveOrders.js @@ -225,7 +225,7 @@ const ActiveOrders = ({ return type === 'caretUp' ? a.price - b.price : b.price - a.price; } }); - setFilteredOrders((prev) => [...prev, ...filteredData]); + setFilteredOrders([...filteredData]); }; return ( diff --git a/web/src/containers/TradeTabs/components/_MarketList.scss b/web/src/containers/TradeTabs/components/_MarketList.scss index d1bcd1069d..75fed8c8bf 100644 --- a/web/src/containers/TradeTabs/components/_MarketList.scss +++ b/web/src/containers/TradeTabs/components/_MarketList.scss @@ -234,6 +234,16 @@ } } } + + .custom-header-wrapper { + color: $colors-black; + font-family: 'Raleway'; + font-weight: bold; + } + .custom-border-bottom { + border-bottom: 1px solid $colors-main-black; + margin-bottom: 10%; + } } .trade_tabs-container { diff --git a/web/src/containers/TransactionsHistory/HistoryDisplay.js b/web/src/containers/TransactionsHistory/HistoryDisplay.js index 746cfc730b..b79897e7b7 100644 --- a/web/src/containers/TransactionsHistory/HistoryDisplay.js +++ b/web/src/containers/TransactionsHistory/HistoryDisplay.js @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { isMobile } from 'react-device-detect'; +import { browserHistory } from 'react-router'; import { TABLE_PAGE_SIZE } from './constants'; import { ActionNotification, @@ -35,6 +36,9 @@ const HistoryDisplay = (props) => { rowKey, expandableRow, expandableContent, + isFromWallet, + onHandleView = () => {}, + isDepositFromWallet, } = props; const [dialogIsOpen, setDialogOpen] = useState(false); @@ -80,13 +84,29 @@ const HistoryDisplay = (props) => { return (
- {!isMobile && !loading && ( + {!loading && (
{title}
- {count > 0 && ( + {!isMobile && !isFromWallet && activeTab === 3 && ( + browserHistory.push('wallet/withdraw')} + /> + )} + {!isMobile && activeTab !== 3 && !isDepositFromWallet && ( + browserHistory.push('wallet/deposit')} + /> + )} + {!isMobile && count > 0 && !isFromWallet && ( { onClick={handleDownload} /> )} - {activeTab === 2 && ( + {!isMobile && activeTab === 2 && !isDepositFromWallet && ( { onClick={openDialog} /> )} - + {!isMobile && !isFromWallet && ( + + )} + {isFromWallet && ( + + )}
)} - {filters} + {!isFromWallet && filters} {loading ? ( ) : ( @@ -134,6 +167,7 @@ const HistoryDisplay = (props) => { jumpToPage={jumpToPage} noData={props.noData} expandable={expandableRow && expandableContent()} + displayPaginator={!isFromWallet} /> )} { @@ -402,7 +421,14 @@ class TransactionsHistory extends Component { }; setActiveTab = (activeTab = 0) => { - const { symbol, orders, trades, withdrawals, deposits } = this.props; + const { + symbol, + orders, + trades, + withdrawals, + deposits, + activeTabFromWallet, + } = this.props; const { jumpToPage } = this.state; if (jumpToPage !== 0) { this.setState({ @@ -430,6 +456,7 @@ class TransactionsHistory extends Component { } } ); + activeTabFromWallet(''); }; withdrawalPopup = (id, amount, currency) => { if (id) { @@ -506,6 +533,16 @@ class TransactionsHistory extends Component { } }; + onHandleView = () => { + const { router, activeTabFromWallet, isDepositFromWallet } = this.props; + if (isDepositFromWallet) { + activeTabFromWallet('deposit'); + } else { + activeTabFromWallet('withdraw'); + } + router.push('/transactions'); + }; + renderActiveTab = () => { const { orders, @@ -517,13 +554,31 @@ class TransactionsHistory extends Component { downloadUserOrders, downloadUserWithdrawal, downloadUserDeposit, + isFromWallet, + isDepositFromWallet, } = this.props; + const filterForWallet = withdrawals.data.filter((item, index) => index < 5); + const filterForDepositWallet = deposits.data.filter( + (item, index) => index < 5 + ); + const withdrawalsForWallet = { + ...withdrawals, + count: 5, + data: filterForWallet, + }; + const depositsForWallet = { + ...deposits, + count: 5, + data: filterForDepositWallet, + }; const { headers, activeTab, filters, jumpToPage, params } = this.state; let temp = params[`activeTab_${activeTab}`]; const props = { symbol, withIcon: true, + isFromWallet, + isDepositFromWallet, }; const prepareNoData = (tab) => { @@ -579,7 +634,7 @@ class TransactionsHistory extends Component { props.stringId = 'TRANSACTION_HISTORY.TITLE_DEPOSITS'; props.title = STRINGS['TRANSACTION_HISTORY.TITLE_DEPOSITS']; props.headers = headers.deposits; - props.data = deposits; + props.data = isDepositFromWallet ? depositsForWallet : deposits; props.filename = `deposit-history-${moment().unix()}`; props.handleNext = this.handleNext; props.jumpToPage = jumpToPage; @@ -587,12 +642,13 @@ class TransactionsHistory extends Component { props.filters = filters.deposits; props.noData = prepareNoData('NO_ACTIVE_DEPOSITS'); props.refetchData = () => this.requestData(activeTab); + props.onHandleView = () => this.onHandleView(); break; case 3: props.stringId = 'TRANSACTION_HISTORY.TITLE_WITHDRAWALS'; props.title = STRINGS['TRANSACTION_HISTORY.TITLE_WITHDRAWALS']; props.headers = headers.withdrawals; - props.data = withdrawals; + props.data = isFromWallet ? withdrawalsForWallet : withdrawals; props.filename = `withdrawal-history-${moment().unix()}`; props.handleNext = this.handleNext; props.jumpToPage = jumpToPage; @@ -600,6 +656,7 @@ class TransactionsHistory extends Component { props.filters = filters.withdrawals; props.noData = prepareNoData('NO_ACTIVE_WITHDRAWALS'); props.refetchData = () => this.requestData(activeTab); + props.onHandleView = () => this.onHandleView(); break; default: return
; @@ -609,7 +666,7 @@ class TransactionsHistory extends Component { }; render() { - const { coins, icons: ICONS } = this.props; + const { coins, icons: ICONS, isFromWallet = false } = this.props; let { activeTab, dialogIsOpen, amount, currency } = this.state; const { onCloseDialog } = this; @@ -626,7 +683,7 @@ class TransactionsHistory extends Component { isMobile && 'overflow-y' )} > - {!isMobile && ( + {!isMobile && !isFromWallet && ( )} - - {STRINGS['TRANSACTION_HISTORY.TRADES']} - - ) : ( -
{string}
} - > - {STRINGS['TRANSACTION_HISTORY.TRADES']} -
- ), - }, - { - title: isMobile ? ( - {STRINGS['ORDER_HISTORY']} - ) : ( -
{string}
} - > - {STRINGS['ORDER_HISTORY']} -
- ), - }, - { - title: isMobile ? ( - - {STRINGS['TRANSACTION_HISTORY.DEPOSITS']} - - ) : ( -
{string}
} - > - {STRINGS['TRANSACTION_HISTORY.DEPOSITS']} -
- ), - }, - { - title: isMobile ? ( - - {STRINGS['TRANSACTION_HISTORY.WITHDRAWALS']} - - ) : ( -
{string}
} - > - {STRINGS['TRANSACTION_HISTORY.WITHDRAWALS']} -
- ), - }, - ]} - activeTab={activeTab} - setActiveTab={this.setActiveTab} - /> + {!isFromWallet && ( + + {STRINGS['TRANSACTION_HISTORY.TRADES']} + + ) : ( +
{string}
} + > + {STRINGS['TRANSACTION_HISTORY.TRADES']} +
+ ), + }, + { + title: isMobile ? ( + {STRINGS['ORDER_HISTORY']} + ) : ( +
{string}
} + > + {STRINGS['ORDER_HISTORY']} +
+ ), + }, + { + title: isMobile ? ( + + {STRINGS['TRANSACTION_HISTORY.DEPOSITS']} + + ) : ( +
{string}
} + > + {STRINGS['TRANSACTION_HISTORY.DEPOSITS']} +
+ ), + }, + { + title: isMobile ? ( + + {STRINGS['TRANSACTION_HISTORY.WITHDRAWALS']} + + ) : ( +
{string}
} + > + {STRINGS['TRANSACTION_HISTORY.WITHDRAWALS']} +
+ ), + }, + ]} + activeTab={activeTab} + setActiveTab={this.setActiveTab} + /> + )} ({ activeLanguage: store.app.language, cancelData: store.wallet.withdrawalCancelData, discount: store.user.discount || 0, + getActiveTabFromWallet: store.wallet.activeTabFromWallet, }); const mapDispatchToProps = (dispatch) => ({ @@ -785,6 +845,7 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(downloadUserTrades('withdrawal', params)), downloadUserOrders: (params) => dispatch(downloadUserTrades('orders', params)), + activeTabFromWallet: bindActionCreators(activeTabFromWallet, dispatch), }); export default connect( diff --git a/web/src/containers/Wallet/AssetsBlock.js b/web/src/containers/Wallet/AssetsBlock.js index 98fd41f932..b8fcac65fc 100644 --- a/web/src/containers/Wallet/AssetsBlock.js +++ b/web/src/containers/Wallet/AssetsBlock.js @@ -3,8 +3,12 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { Link } from 'react-router'; import { isMobile } from 'react-device-detect'; -import { Switch } from 'antd'; -import { CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons'; +import { Checkbox, Switch } from 'antd'; +import { + CaretUpOutlined, + CaretDownOutlined, + SearchOutlined, +} from '@ant-design/icons'; import { fetchBalanceHistory, fetchPlHistory } from './actions'; import classnames from 'classnames'; import Highcharts from 'highcharts'; @@ -66,6 +70,7 @@ const AssetsBlock = ({ handleBalanceHistory, balance_history_config, info, + setActiveTab, }) => { const emptyDonut = useMemo(() => { return chartData && !!chartData.length; @@ -75,6 +80,7 @@ const AssetsBlock = ({ const [historyData, setHistoryData] = useState([]); const [userPL, setUserPL] = useState(); const [plLoading, setPlLoading] = useState(false); + const [isSearchActive, setIsSearchActive] = useState(false); const handleUpgrade = (info = {}) => { if ( @@ -131,6 +137,9 @@ const AssetsBlock = ({ .catch((error) => { setPlLoading(false); }); + return () => { + setIsSearchActive(false); + }; // eslint-disable-next-line }, []); @@ -290,40 +299,51 @@ const AssetsBlock = ({ : 'wallet-assets_block empty-wallet-assets_block' } > -
- {totalAssets.length && !loading ? ( - ( -
- {BASE_CURRENCY && ( -
-
{STRINGS['WALLET_ESTIMATED_TOTAL_BALANCE']}
-
{totalAssets}
-
- )} +
+
+
+ {!isSearchActive ? ( +
+ + + {STRINGS['BALANCES']} + + + setIsSearchActive(true)} + > + + +
+ ) : ( +
+ + +
)} - > - {STRINGS['WALLET_ESTIMATED_TOTAL_BALANCE']} - - ) : ( -
-
{STRINGS['WALLET_BALANCE_LOADING']}
-
- )} -
- - - -
-
+
+
+ + + {STRINGS['WALLET_HIDE_ZERO_BALANCE']} + + + + onToggle(e.target.checked)} + checked={isZeroBalanceHidden} + > + +
+
-
- - - {STRINGS['WALLET_HIDE_ZERO_BALANCE']} - - - -
@@ -576,25 +578,38 @@ const AssetsBlock = ({
- + + )} + {!isMobile && ( + + )} + {!isMobile && ( + + )} {!isMobile && ( - + ) : ( + + )} + {!isMobile && - - { + + )} + {!isMobile && ( - } + )} {/* {hasEarn && (
- + - {STRINGS['CURRENCY']} + {isMobile + ? STRINGS['MARKETS_TABLE.ASSET'] + : STRINGS['CURRENCY']} -
- - {STRINGS['AMOUNT']} + {isMobile && ( +
+ + {STRINGS['WALLET.MOBILE_WALLET_BALANCE_LABEL']} - {renderCaret(WALLET_SORT.AMOUNT)} - - +
+ + {STRINGS['AMOUNT']} + + {renderCaret(WALLET_SORT.AMOUNT)} +
+
- - - {STRINGS['DEPOSIT_WITHDRAW']} - - + + {STRINGS['DEPOSIT_WITHDRAW']} + + @@ -644,20 +659,47 @@ const AssetsBlock = ({ {assets && !loading ? ( -
- - - - -
- - {fullname} - -
- -
+ isMobile ? ( +
+ + + + +
+ + {symbol?.toUpperCase()} + +
+
+ + {fullname} + +
+ +
+ ) : ( +
+ + + + +
+ + {fullname} + +
+ +
+ ) ) : (
)}
- {assets && baseCoin && !loading && increment_unit ? ( -
-
- {STRINGS.formatString( - CURRENCY_PRICE_FORMAT, - formatCurrencyByIncrementalUnit( - balance, - increment_unit - ), - display_name - )} + {isMobile ? ( +
+ {assets && baseCoin && !loading && increment_unit ? ( +
+
+
{balance}
+ {key !== BASE_CURRENCY && + parseFloat(balanceText || 0) > 0 && ( +
+ {`(≈ $${balanceText})`} +
+ )} +
+
+ +
- {!isMobile && - key !== BASE_CURRENCY && - parseFloat(balanceText || 0) > 0 && ( -
- {`(≈ ${baseCoin.display_name} ${balanceText})`} -
- )} + ) : ( +
+ )} +
+ {assets && baseCoin && !loading && increment_unit ? ( +
+
+ {STRINGS.formatString( + CURRENCY_PRICE_FORMAT, + formatCurrencyByIncrementalUnit( + balance, + increment_unit + ), + display_name + )} +
+ {key !== BASE_CURRENCY && + parseFloat(balanceText || 0) > 0 && ( +
+ {`(≈ ${baseCoin.display_name} ${balanceText})`} +
+ )} +
+ ) : ( +
+ )} +
} + {!isMobile && ( + +
+ navigate(`wallet/${key}/deposit`)} + className="csv-action action-button-wrapper" + showActionText={isMobile} + disable={!allow_deposit} + /> + navigate(`wallet/${key}/withdraw`)} + className="csv-action action-button-wrapper" + showActionText={isMobile} + disable={!allow_withdrawal} + />
- ) : ( -
- )} -
- -
- navigate(`wallet/${key}/deposit`)} - className="csv-action action-button-wrapper" - showActionText={isMobile} - disable={!allow_deposit} - /> - navigate(`wallet/${key}/withdraw`)} - className="csv-action action-button-wrapper" - showActionText={isMobile} - disable={!allow_withdrawal} - /> -
-
{markets.length > 1 ? ( )}
+ + {isMobile && ( +
+
navigate('/wallet/history')}> + + + {STRINGS['WALLET.VIEW_MORE_WALLET_INFO']} + + +
+ +
setActiveTab(1)}> + + + {STRINGS['WALLET.VIEW_WALLET_TRANSACTION_HISTORY']} + + +
+
+ )}
); diff --git a/web/src/containers/Wallet/HeaderSection.js b/web/src/containers/Wallet/HeaderSection.js index 676203cdf2..c6a63c7ced 100644 --- a/web/src/containers/Wallet/HeaderSection.js +++ b/web/src/containers/Wallet/HeaderSection.js @@ -2,23 +2,39 @@ import React from 'react'; import { Link } from 'react-router'; import { EditWrapper, Image } from 'components'; import STRINGS from 'config/localizedStrings'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { setDepositAndWithdraw } from 'actions/appActions'; -const HeaderSection = ({ icons: ICONS }) => { +const HeaderSection = ({ icons: ICONS, setDepositAndWithdraw }) => { return ( -
+
{STRINGS['ACCORDIAN.ACCORDIAN_ASSETS']}
-
- - - {STRINGS['ACCORDIAN.ACCORDIAN_INFO']} - - -
+
setDepositAndWithdraw(true)} + > + + + {STRINGS['ACCORDIAN.DEPOSIT']} + + +
+
setDepositAndWithdraw(true)} + > + + + {STRINGS['ACCORDIAN.WITHDRAW']} + + +
@@ -39,4 +55,9 @@ const HeaderSection = ({ icons: ICONS }) => { ); }; -export default HeaderSection; +const mapDispatchToProps = (dispatch) => ({ + setDepositAndWithdraw: bindActionCreators(setDepositAndWithdraw, dispatch), + dispatch, +}); + +export default connect('', mapDispatchToProps)(HeaderSection); diff --git a/web/src/containers/Wallet/MainWallet.js b/web/src/containers/Wallet/MainWallet.js index c1d5a662a1..3ece989196 100644 --- a/web/src/containers/Wallet/MainWallet.js +++ b/web/src/containers/Wallet/MainWallet.js @@ -3,7 +3,14 @@ import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import classnames from 'classnames'; import { isMobile } from 'react-device-detect'; -import { IconTitle, Accordion, MobileBarTabs, NotLoggedIn } from 'components'; +import { + IconTitle, + Accordion, + MobileBarTabs, + NotLoggedIn, + Button, + MobileBarBack, +} from 'components'; import { TransactionsHistory, Stake } from 'containers'; import { changeSymbol } from 'actions/orderbookAction'; import { @@ -190,6 +197,7 @@ class Wallet extends Component { goToWallet={this.goToWallet} isZeroBalanceHidden={isZeroBalanceHidden} handleBalanceHistory={this.handleBalanceHistory} + setActiveTab={this.setActiveTab} /> ), isOpen: true, @@ -222,6 +230,9 @@ class Wallet extends Component { coins={coins} searchResult={this.getMobileSlider(coins, oraclePrices)} router={this.props.router} + totalAssets={totalAssets} + loading={isFetching} + BASE_CURRENCY={BASE_CURRENCY} /> ), }, @@ -257,9 +268,16 @@ class Wallet extends Component { this.setState({ showDustSection: false }); }; + goBack = () => { + this.handleBalanceHistory(false); + }; + render() { const { sections, activeTab, mobileTabs, showDustSection } = this.state; - const { icons: ICONS } = this.props; + const { icons: ICONS, router, assets, isFetching, pairs } = this.props; + + const isNotWalletHistory = router?.location?.pathname !== '/wallet/history'; + const isWalletHistory = router?.location?.pathname === '/wallet/history'; if (mobileTabs.length === 0) { return
; @@ -268,11 +286,16 @@ class Wallet extends Component {
{isMobile ? (
- + {isNotWalletHistory && ( + + )} + {isWalletHistory && ( + this.goBack()} /> + )}
{mobileTabs[activeTab].content}
@@ -309,6 +332,10 @@ class Wallet extends Component { ) : ( )} @@ -317,6 +344,28 @@ class Wallet extends Component {
)} + {isMobile && + router?.location?.pathname === '/wallet' && + this.state.activeTab === 0 && ( +
+
+
+
+
+
+ )}
); } diff --git a/web/src/containers/Wallet/MobileWallet.js b/web/src/containers/Wallet/MobileWallet.js index 52cf3e5e27..99c33aa62e 100644 --- a/web/src/containers/Wallet/MobileWallet.js +++ b/web/src/containers/Wallet/MobileWallet.js @@ -1,8 +1,9 @@ import React, { useEffect, useState } from 'react'; import classnames from 'classnames'; -import { Accordion } from '../../components'; +import { Accordion, EditWrapper } from '../../components'; import CurrencySlider from './components/CurrencySlider'; import ProfitLossSection from './ProfitLossSection'; +import strings from 'config/localizedStrings'; const MobileWallet = ({ sections, @@ -12,6 +13,9 @@ const MobileWallet = ({ coins, searchResult, router, + totalAssets, + loading, + BASE_CURRENCY, }) => { const [activeBalanceHistory, setActiveBalanceHistory] = useState(false); @@ -21,6 +25,9 @@ const MobileWallet = ({ } }, []); + const isNotWalletHistory = router?.location?.pathname !== '/wallet/history'; + const isWalletHistory = router?.location?.pathname === '/wallet/history'; + const handleBalanceHistory = (value) => { setActiveBalanceHistory(value); if (value) { @@ -42,22 +49,54 @@ const MobileWallet = ({ 'w-100' )} > -
- -
+ {isNotWalletHistory && totalAssets.length && !loading ? ( +
+ ( +
+ {BASE_CURRENCY && ( +
+
+ {strings['WALLET_ESTIMATED_TOTAL_BALANCE']} +
+
+ {totalAssets} +
+
+ )} +
+ )} + > + {strings['WALLET_ESTIMATED_TOTAL_BALANCE']} +
+
+ ) : ( + isNotWalletHistory && ( +
+
{strings['WALLET_BALANCE_LOADING']}
+
+
+ ) + )} + {isNotWalletHistory && ( +
+ +
+ )}
- {!activeBalanceHistory ? ( - + {activeBalanceHistory && isWalletHistory ? ( + ) : ( -
- +
+
)}
diff --git a/web/src/containers/Wallet/ProfitLossSection.js b/web/src/containers/Wallet/ProfitLossSection.js index 4ab0fc77b5..89afc2ed28 100644 --- a/web/src/containers/Wallet/ProfitLossSection.js +++ b/web/src/containers/Wallet/ProfitLossSection.js @@ -4,7 +4,7 @@ import withConfig from 'components/ConfigProvider/withConfig'; import Highcharts from 'highcharts'; import HighchartsReact from 'highcharts-react-official'; // eslint-disable-next-line -import { Coin, EditWrapper } from 'components'; +import { Coin, DonutChart, EditWrapper, MobileBarBack } from 'components'; import { Link } from 'react-router'; import { Button, Spin, DatePicker, message, Modal, Tabs } from 'antd'; import { fetchBalanceHistory, fetchPlHistory } from './actions'; @@ -13,6 +13,13 @@ import moment from 'moment'; import STRINGS from 'config/localizedStrings'; import { CloseOutlined } from '@ant-design/icons'; import './_ProfitLoss.scss'; +import { isMobile } from 'react-device-detect'; +import { BASE_CURRENCY, DEFAULT_COIN_DATA } from 'config/constants'; +import { assetsSelector } from './utils'; +import { + calculateOraclePrice, + formatCurrencyByIncrementalUnit, +} from 'utils/currency'; const TabPane = Tabs.TabPane; const ProfitLossSection = ({ coins, @@ -20,6 +27,9 @@ const ProfitLossSection = ({ handleBalanceHistory, balances, pricesInNative, + chartData, + assets, + loading, }) => { const month = Array.apply(0, Array(12)).map(function (_, i) { return moment().month(i).format('MMM'); @@ -50,6 +60,7 @@ const ProfitLossSection = ({ const [customDate, setCustomDate] = useState(false); const [customDateValues, setCustomDateValues] = useState(); const [loadingPnl, setLoadingPnl] = useState(false); + const [activeTab, setActiveTab] = useState('0'); const options = { chart: { @@ -272,6 +283,12 @@ const ProfitLossSection = ({ return ( <> {sortedCoins.map((coin, index) => { + const { symbol } = coin; + const baseCoin = coins[BASE_CURRENCY] || DEFAULT_COIN_DATA; + const selectedCoin = assets.find((coin) => coin[0] === symbol); + const { increment_unit } = selectedCoin; + const oraclePrice = pricesInNative[coin?.symbol]; + const balance = balances[`${coin?.symbol}_balance`]; const incrementUnit = coins[coin.symbol].increment_unit; const decimalPoint = new BigNumber(incrementUnit).dp(); const sourceAmount = new BigNumber( @@ -279,7 +296,6 @@ const ProfitLossSection = ({ ) .decimalPlaces(decimalPoint) .toNumber(); - const incrementUnitNative = coins[balance_history_config?.currency || 'usdt'].increment_unit; const decimalPointNative = new BigNumber(incrementUnitNative).dp(); @@ -288,7 +304,16 @@ const ProfitLossSection = ({ ) .decimalPlaces(decimalPointNative) .toNumber(); - + const balanceText = + coin?.symbol === BASE_CURRENCY + ? formatCurrencyByIncrementalUnit(balance, increment_unit) + : formatCurrencyByIncrementalUnit( + calculateOraclePrice(balance, oraclePrice), + baseCoin.increment_unit + ); + const getBalancePercentage = chartData.filter( + (coin) => coin.symbol === symbol + ); if (sourceAmount > 0) { return ( @@ -304,29 +329,72 @@ const ProfitLossSection = ({ to={`/prices/coin/${coin.symbol}`} className="underline" > -
- -
{coin.display_name}
-
+ {isMobile ? ( +
+ +
+
+ {coin.display_name} +
+
{coin?.fullname}
+
+
+ ) : ( +
+ +
{coin.display_name}
+
+ )} - {sourceAmount} {coin.symbol.toUpperCase()} +
+ {sourceAmount} +
+ {isMobile && + selectedCoin[0] !== BASE_CURRENCY && + parseFloat(balanceText || 0) > 0 && ( +
{`(≈ $${balanceText})`}
+ )} - - = {sourceAmountNative}{' '} - {balance_history_config?.currency?.toUpperCase() || 'USDT'} - + {!isMobile && ( + + = {sourceAmountNative}{' '} + {balance_history_config?.currency?.toUpperCase() || 'USDT'} + + )} + {!isMobile && ( + +
{getBalancePercentage[0]?.balancePercentage}
+ {isMobile && + selectedCoin[0] !== BASE_CURRENCY && + parseFloat(balanceText || 0) > 0 && ( +
{`(≈ $${balanceText})`}
+ )} + + )} ); } @@ -537,36 +605,43 @@ const ProfitLossSection = ({ return '1m'; } else return '3m'; }; + + const onHandleTab = (activeKey) => { + setActiveTab(activeKey); + }; + return ( -
+
{customDateModal()} - -
- handleBalanceHistory(false)} - > - {'<'} - - {STRINGS['PROFIT_LOSS.BACK']} + {!isMobile && ( +
+ handleBalanceHistory(false)} + > + {'<'} + + {STRINGS['PROFIT_LOSS.BACK']} + + {' '} + + {STRINGS['PROFIT_LOSS.BACK_TO_WALLET']} - {' '} - - {STRINGS['PROFIT_LOSS.BACK_TO_WALLET']} - -
- +
+ )} +
-
+
{STRINGS['PROFIT_LOSS.WALLET_PERFORMANCE_TITLE']} @@ -586,155 +661,409 @@ const ProfitLossSection = ({
-
+ {!isMobile && (
- - {STRINGS['PROFIT_LOSS.EST_TOTAL_BALANCE']} - {' '} - {moment(latestBalance?.created_at).format('DD/MMM/YYYY')} -
-
- {balance_history_config?.currency?.toUpperCase() || 'USDT'}{' '} - {getSourceDecimals( - balance_history_config?.currency || 'usdt', - latestBalance?.total - ) - ?.toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0'} -
- -
0 - ? 'profitPositive' - : 'profitNegative' - } - > - {' '} - {currentDay + ' '} - - {STRINGS['PROFIT_LOSS.PL_DAYS']} +
+ + {STRINGS['PROFIT_LOSS.EST_TOTAL_BALANCE']} {' '} - {Number(userPL?.[getPeriod(currentDay)]?.total || 0) > 0 - ? '+' - : ' '} - {''} + {moment(latestBalance?.created_at).format('DD/MMM/YYYY')} +
+
+ {balance_history_config?.currency?.toUpperCase() || + 'USDT'}{' '} {getSourceDecimals( balance_history_config?.currency || 'usdt', - userPL?.[getPeriod(currentDay)]?.total + latestBalance?.total ) ?.toString() .replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0'} - {userPL?.[getPeriod(currentDay)]?.totalPercentage - ? ` (${ - userPL?.[getPeriod(currentDay)]?.totalPercentage - }%) ` - : ' '} - {balance_history_config?.currency?.toUpperCase() || - 'USDT'}
- -
+ +
0 + ? 'profitPositive' + : 'profitNegative' + } + > + {' '} + {currentDay + ' '} + + {STRINGS['PROFIT_LOSS.PL_DAYS']} + {' '} + {Number(userPL?.[getPeriod(currentDay)]?.total || 0) > 0 + ? '+' + : ' '} + {''} + {getSourceDecimals( + balance_history_config?.currency || 'usdt', + userPL?.[getPeriod(currentDay)]?.total + ) + ?.toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0'} + {userPL?.[getPeriod(currentDay)]?.totalPercentage + ? ` (${ + userPL?.[getPeriod(currentDay)]?.totalPercentage + }%) ` + : ' '} + {balance_history_config?.currency?.toUpperCase() || + 'USDT'} +
+
+
+ )}
-
- - - + + + +
+ {isMobile && ( +
0 + ? 'profitPositive mb-5' + : 'profitNegative mb-5' + } + > + {' '} + {currentDay + ' '} + + {STRINGS['PROFIT_LOSS.PL_DAYS']} + {' '} + {Number(userPL?.[getPeriod(currentDay)]?.total || 0) > 0 + ? '+' + : ' '} + {''} + {getSourceDecimals( + balance_history_config?.currency || 'usdt', + userPL?.[getPeriod(currentDay)]?.total + ) + ?.toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0'} + {userPL?.[getPeriod(currentDay)]?.totalPercentage + ? ` (${ + userPL?.[getPeriod(currentDay)]?.totalPercentage + }%) ` + : ' '} + {balance_history_config?.currency?.toUpperCase() || 'USDT'} +
+ )} {/* */} + style={{ + fontWeight: currentDay === 'custom' ? 'bold' : '400', + fontSize: '1em', + }} + className="plButton" + ghost + onClick={() => { + setCustomDate(true); + }} + > + + {STRINGS['PROFIT_LOSS.CUSTOM']} + + */}
+ {isMobile && ( +
+
+
+ + {STRINGS['PROFIT_LOSS.WALLET_BALANCE_ESTIMATE']} + + + {' '} + ( + {moment(latestBalance?.created_at).format( + 'DD/MMM/YYYY' + )} + ): + +
+
+ {balance_history_config?.currency?.toUpperCase() || + 'USDT'}{' '} + {getSourceDecimals( + balance_history_config?.currency || 'usdt', + latestBalance?.total + ) + ?.toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0'} +
+
+
setActiveTab('2')}> + + + {STRINGS['PROFIT_LOSS.VIEW_BALANCE_HISTORY']} + + +
+ +
setActiveTab('1')}> + + + {STRINGS['PROFIT_LOSS.VIEW_PERCENTAGE_SHARE']} + + +
+
+ )}
- {currentBalance && ( - + {isMobile && ( +
+
+ +
+
+ + + + + + + + + {assets.map( + ( + [ + key, + { + increment_unit, + oraclePrice, + balance, + fullname, + symbol = '', + icon_id, + } = DEFAULT_COIN_DATA, + ], + index + ) => { + // const markets = getAllAvailableMarkets(key); + const getBalancePercentage = chartData.filter( + (coin) => coin.symbol === symbol + ); + const baseCoin = + coins[BASE_CURRENCY] || DEFAULT_COIN_DATA; + const balanceText = + key === BASE_CURRENCY + ? formatCurrencyByIncrementalUnit( + balance, + increment_unit + ) + : formatCurrencyByIncrementalUnit( + calculateOraclePrice(balance, oraclePrice), + baseCoin.increment_unit + ); + return ( + + + + + ); + } + )} + +
+ + + {STRINGS['ASSETS']} + + + + {STRINGS['WALLET.MOBILE_WALLET_SHARE_LABEL']} + +
+ + {assets && !loading ? ( +
+ + + + +
+ + {symbol?.toUpperCase()} + +
+
+ + {fullname} + +
+ +
+ ) : ( +
+ )} +
+ {assets && + baseCoin && + !loading && + increment_unit ? ( +
+
+
+ { + getBalancePercentage[0] + ?.balancePercentage + } +
+ {key !== BASE_CURRENCY && + parseFloat(balanceText || 0) > 0 && ( +
+ {`(≈ $${balanceText})`} +
+ )} +
+
+ ) : ( +
+ )} +
+
+ {isMobile && ( +
+
setActiveTab('2')}> + + + {STRINGS['PROFIT_LOSS.VIEW_BALANCE_HISTORY']} + + +
+
setActiveTab('0')}> + + + {STRINGS['PROFIT_LOSS.VIEW_WALLET_P&L']} + + +
+
+ )} +
+
+ )} + {currentBalance && ( + +
-
-
- - {STRINGS['PROFIT_LOSS.WALLET_BALANCE']} - -
-
- - {STRINGS['PROFIT_LOSS.WALLET_BALANCE_DESCRIPTION_1']} - {' '} - {moment(currentBalance?.created_at).format('DD/MMM/YYYY')} - . - - {STRINGS['PROFIT_LOSS.WALLET_BALANCE_DESCRIPTION_2']} - -
-
-
+ {!isMobile && (
- - {STRINGS['PROFIT_LOSS.EST_TOTAL_BALANCE']} - {' '} - {moment(currentBalance?.created_at).format('DD/MMM/YYYY')} +
+ + {STRINGS['PROFIT_LOSS.WALLET_BALANCE']} + +
+
+ + {STRINGS['PROFIT_LOSS.WALLET_BALANCE_DESCRIPTION_1']} + {' '} + {moment(currentBalance?.created_at).format( + 'DD/MMM/YYYY' + )} + . + + {STRINGS['PROFIT_LOSS.WALLET_BALANCE_DESCRIPTION_2']} + +
-
- {balance_history_config?.currency?.toUpperCase() || - 'USDT'}{' '} - {getSourceDecimals( - balance_history_config?.currency || 'usdt', - currentBalance?.total - ) - ?.toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0'} + )} +
+
+
+ + {STRINGS['PROFIT_LOSS.EST_TOTAL_BALANCE']} + {' '} + {moment(currentBalance?.created_at).format( + 'DD/MMM/YYYY' + )} +
+
+ {balance_history_config?.currency?.toUpperCase() || + 'USDT'}{' '} + {getSourceDecimals( + balance_history_config?.currency || 'usdt', + currentBalance?.total + ) + ?.toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ',') || '0'} +
-
+
{STRINGS['PROFIT_LOSS.DATE_SELECT']} @@ -847,15 +1186,32 @@ const ProfitLossSection = ({ - - {STRINGS['PROFIT_LOSS.BALANCE_AMOUNT']} - - - - - {STRINGS['PROFIT_LOSS.VALUE']} + + {isMobile + ? STRINGS['WALLET.MOBILE_WALLET_BALANCE_LABEL'] + : STRINGS['PROFIT_LOSS.BALANCE_AMOUNT']} + {!isMobile && ( + + + {STRINGS['PROFIT_LOSS.VALUE']} + + + )} + {!isMobile && ( + + + {STRINGS['PROFIT_LOSS.BALANCE_PERCENTAGE']} + + + )} @@ -863,6 +1219,25 @@ const ProfitLossSection = ({
+ {isMobile && ( +
+
setActiveTab('0')}> + + + {STRINGS['PROFIT_LOSS.VIEW_WALLET_P&L']} + + +
+ +
setActiveTab('1')}> + + + {STRINGS['PROFIT_LOSS.VIEW_PERCENTAGE_SHARE']} + + +
+
+ )}
)} @@ -878,6 +1253,9 @@ const mapStateToProps = (state) => ({ pricesInNative: state.asset.oraclePrices, dust: state.app.constants.dust, balance_history_config: state.app.constants.balance_history_config, + chartData: state.asset.chartData, + assets: assetsSelector(state), + quickTrade: state.app.quickTrade, }); const mapDispatchToProps = (dispatch) => ({}); diff --git a/web/src/containers/Wallet/_ProfitLoss.scss b/web/src/containers/Wallet/_ProfitLoss.scss index 088cdabaa0..731a18d695 100644 --- a/web/src/containers/Wallet/_ProfitLoss.scss +++ b/web/src/containers/Wallet/_ProfitLoss.scss @@ -30,6 +30,30 @@ fill: rgba(255, 255, 255, 0.07); } +.profit-loss-chart-wrapper { + background-color: var(--base_wallet-sidebar-and-popup); + padding: 2% 5% 5% 5%; + .profitNegative { + color: var(--trading_selling-related-elements); + } + + .profitPositive { + color: var(--trading_buying-related-elements); + } + + .profitNeutral { + color: '#ccc'; + } + + .caret-right-icon { + padding-bottom: 1%; + } + + .view-more-wrapper { + color: var(--labels_important-active-labels-text-graphics); + } +} + .profitNegative { color: var(--trading_selling-related-elements); } @@ -123,3 +147,39 @@ color: var(--labels_important-active-labels-text-graphics); } } + +.layout-mobile { + .wallet-assets_block { + .balance-history { + display: flex; + flex-direction: column-reverse; + + .total-balance { + display: flex; + flex-direction: column; + } + } + + .balance-history-date_select { + display: flex; + gap: 5px; + align-items: center; + margin-bottom: 2rem; + } + } + + .profit-loss-link { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + + .profit-loss-tab-label { + cursor: pointer !important; + text-decoration: underline !important; + color: var(--specials_buttons-links-and-highlights); + font-size: 12px !important; + } + } +} diff --git a/web/src/containers/Wallet/_Wallet.scss b/web/src/containers/Wallet/_Wallet.scss index 844e5837df..1a49571677 100644 --- a/web/src/containers/Wallet/_Wallet.scss +++ b/web/src/containers/Wallet/_Wallet.scss @@ -72,6 +72,19 @@ $header-margin: 2rem; animation: dotFlashing 3s infinite; } } + .estimated-balance-wrapper { + .balance-wrapper { + color: $colors-main-black; + .estimated-balance-label { + font-size: 16px; + font-weight: bold; + } + .estimated-balance-amount { + font-family: 'Raleway'; + font-size: 4rem; + } + } + } .wallet-container { .action_notification-wrapper { .action_notification-text { @@ -96,11 +109,17 @@ $header-margin: 2rem; } .wallet-assets_block { padding: 6px; + .profit-loss-preformance { + color: $colors-main-black; + } .wallet-assets_block-table { thead { tr { + .mobile-balance-header { + text-align: end !important; + } th:nth-child(3) { - text-align: center; + text-align: end; } } } @@ -109,6 +128,15 @@ $header-margin: 2rem; .td-name { min-width: unset !important; } + .td-amount { + .more-icon-wrapper { + svg { + width: 3rem; + height: 3rem; + fill: $colors-main-black !important; + } + } + } .td-wallet { .deposit-withdrawal-wrapper { margin: 0 auto; @@ -135,6 +163,64 @@ $header-margin: 2rem; } .zero-balance-wrapper { flex-direction: column; + font-weight: bold; + color: $colors-main-black; + .edit-wrapper__container { + .tooltip_icon_wrapper div { + width: 100%; + } + } + .wallet-search-label { + font-size: 14px; + } + .wallet-search-icon { + padding: 1.5%; + border-radius: 20px; + align-items: center; + display: flex; + justify-content: center; + background-color: $link; + } + .wallet-search-field { + .edit-wrapper__container { + width: 100%; + .field-children div div { + width: 6%; + } + + .field-children div input { + width: 100%; + } + + .field-content-outline:before, + .field-content-outline:after { + height: unset !important; + } + + .field-children { + border: 1px solid $colors-black; + border-radius: 5px; + } + } + .field-error-content { + display: none; + } + .edit-wrapper__container div:nth-child(1) { + width: 100%; + } + } + .hide-zero-balance-checkbox { + .ant-checkbox-inner { + background-color: var(--base_wallet-sidebar-and-popup); + } + .ant-checkbox-inner:focus, + .ant-checkbox-inner:focus-within { + border-color: $colors-main-black !important; + } + } + .hide-zero-balance-field { + gap: 10px; + } } } .action-button-wrapper { @@ -169,6 +255,17 @@ $header-margin: 2rem; } } @media screen and (max-width: 550px) { + .wallet-container { + .wallet-assets_block { + .zero-balance-wrapper { + .edit-wrapper__container { + .tooltip_icon_wrapper div { + width: 1.5rem; + } + } + } + } + } .button-container { .holla-button { font-size: 10px !important; @@ -262,7 +359,16 @@ $header-margin: 2rem; } .wallet-container { + .fill_secondary-color { + color: $colors-black; + } + + .fill-active-color { + color: $colors-main-black; + } + .header-wrapper { + border-top: 1px solid $colors-main-black; .image-Wrapper { .action_notification-svg svg { width: 1.25rem; @@ -276,9 +382,15 @@ $header-margin: 2rem; .action_notification-svg { padding-top: 7px; } + + .link { + border-right: 1px solid $colors-border; + padding-right: 0.5rem; + padding-left: 0.5rem; + } } - border-top: $wrapper-border; + // border-top: $wrapper-border; // border-bottom: $wrapper-border; &.no-border { @@ -692,3 +804,31 @@ $header-margin: 2rem; } } } + +.layout-mobile { + .footer-button { + margin-top: 4%; + margin-bottom: 2%; + } + .bottom-bar-button { + padding: 3%; + display: flex; + justify-content: space-around; + + .bottom-bar-deposit-button, + .bottom-bar-withdraw-button { + width: 45%; + button { + border-radius: 5px; + } + } + .bottom-bar-withdraw-button { + button { + color: $colors-main-black; + background-color: $app-sidebar-background !important; + border-radius: 5px; + border-color: $colors-main-black !important; + } + } + } +} diff --git a/web/src/containers/Wallet/components/CurrencySlider.js b/web/src/containers/Wallet/components/CurrencySlider.js index 34130ff106..a35b04b987 100644 --- a/web/src/containers/Wallet/components/CurrencySlider.js +++ b/web/src/containers/Wallet/components/CurrencySlider.js @@ -1,16 +1,13 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; -// import classnames from 'classnames'; import { isMobile } from 'react-device-detect'; +import { CaretRightOutlined } from '@ant-design/icons'; import { Button, // DonutChart, EditWrapper, } from 'components'; -import { calculatePrice } from 'utils/currency'; -import { BASE_CURRENCY, DEFAULT_COIN_DATA } from 'config/constants'; -import Currency from './Currency'; -import Arrow from './Arrow'; +import { DEFAULT_COIN_DATA } from 'config/constants'; import STRINGS from 'config/localizedStrings'; import _toLower from 'lodash/toLower'; import { fetchBalanceHistory, fetchPlHistory } from '../actions'; @@ -122,18 +119,19 @@ class CurrencySlider extends Component { render() { const { - balance, - prices, + // balance, + // prices, navigate, coins, - searchResult, + // searchResult, // chartData, + handleBalanceHistory, } = this.props; const { currentCurrency } = this.state; - const balanceValue = balance[`${currentCurrency}_balance`]; - const baseBalance = - currentCurrency !== BASE_CURRENCY && - calculatePrice(balanceValue, prices[currentCurrency]); + // const balanceValue = balance[`${currentCurrency}_balance`]; + // const baseBalance = + // currentCurrency !== BASE_CURRENCY && + // calculatePrice(balanceValue, prices[currentCurrency]); const { fullname, allow_deposit, allow_withdrawal } = coins[currentCurrency] || DEFAULT_COIN_DATA; @@ -249,7 +247,7 @@ class CurrencySlider extends Component {
)}
*/} -
+
{!isUpgrade && this.props.balance_history_config?.active && Number(this.state.userPL?.['7d']?.total || 0) !== 0 && @@ -258,8 +256,6 @@ class CurrencySlider extends Component {
@@ -267,24 +263,6 @@ class CurrencySlider extends Component { {STRINGS['PROFIT_LOSS.PERFORMANCE_TREND']}
-
- {STRINGS['PROFIT_LOSS.WALLET_PERFORMANCE_TITLE']} -
- -
{ - this.props.handleBalanceHistory(true); - }} - style={{ zoom: 0.3, cursor: 'pointer' }} - className="highChartColor highChartColorOverview" - > - {' '} - {' '} -
-
{ this.props.handleBalanceHistory(true); @@ -297,31 +275,64 @@ class CurrencySlider extends Component { : 'profitNegative' } style={{ - marginTop: 10, + marginTop: 5, display: 'flex', - justifyContent: 'center', fontSize: '1.5rem', cursor: 'pointer', }} > - - {STRINGS['PROFIT_LOSS.PL_7_DAY']} - {' '} - {Number(this.state.userPL?.['7d']?.total || 0) > 0 - ? '+' - : ' '} - {''} - {getSourceDecimals( - this.props.balance_history_config?.currency || 'usdt', - this.state.userPL?.['7d']?.total - ?.toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ',') - ) || '0'} - {this.state.userPL?.['7d']?.totalPercentage - ? ` (${this.state.userPL?.['7d']?.totalPercentage}%) ` - : ' '} - {this.props.balance_history_config?.currency?.toUpperCase() || - 'USDT'} +
+ + {STRINGS['PROFIT_LOSS.PL_7_DAY']} + {' '} + {Number(this.state.userPL?.['7d']?.total || 0) > 0 + ? '+' + : ' '} + {''} + {getSourceDecimals( + this.props.balance_history_config?.currency || 'usdt', + this.state.userPL?.['7d']?.total + ?.toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ',') + ) || '0'} + {this.state.userPL?.['7d']?.totalPercentage + ? ` (${this.state.userPL?.['7d']?.totalPercentage}%) ` + : ' '} + {this.props.balance_history_config?.currency?.toUpperCase() || + 'USDT'} +
+
+
handleBalanceHistory(true)} + > + + {STRINGS['HOLLAEX_TOKEN.VIEW']} + + + + +
+
+
+
+ {STRINGS['PROFIT_LOSS.WALLET_PERFORMANCE_TITLE']} +
+ +
handleBalanceHistory(true)} + style={{ zoom: 0.3, cursor: 'pointer' }} + className="highChartColor highChartColorOverview" + > + {' '} + {' '}
)} @@ -329,7 +340,7 @@ class CurrencySlider extends Component {
)} -
+ {/*
this.previousCurrency()} />
@@ -346,7 +357,7 @@ class CurrencySlider extends Component {
this.nextCurrency()} />
-
+
*/} {!isMobile && (
diff --git a/web/src/containers/Wallet/components/TradeInputGroup.js b/web/src/containers/Wallet/components/TradeInputGroup.js index aead759b22..7c96f3baba 100644 --- a/web/src/containers/Wallet/components/TradeInputGroup.js +++ b/web/src/containers/Wallet/components/TradeInputGroup.js @@ -13,6 +13,7 @@ const TradeInputGroup = ({ icons: ICONS, pairs, tradeClassName, + text, }) => { return ( ); +export const renderDeposit = (renderDeposit) => ( + renderDeposit()} + className="render-deposit mt-1" + /> +); + export const renderInformation = ( symbol = BASE_CURRENCY, balance, @@ -216,7 +226,7 @@ export const renderTitleSection = (symbol, type, icon, coins, iconId) => { diff --git a/web/src/containers/Withdraw/Fiat/Form.js b/web/src/containers/Withdraw/Fiat/Form.js index 6860cd1438..78bf5701d4 100644 --- a/web/src/containers/Withdraw/Fiat/Form.js +++ b/web/src/containers/Withdraw/Fiat/Form.js @@ -28,8 +28,12 @@ class Form extends Component { coins, banks, prices, + getWithdrawCurrency, } = this.props; const { activeTab } = this.state; + const currentCurrency = getWithdrawCurrency + ? getWithdrawCurrency + : currency; let initialBank; if (banks && banks.length === 1) { @@ -38,7 +42,7 @@ class Form extends Component { this.generateFormValues( activeTab, - currency, + currentCurrency, balance, coins, verification_level, @@ -49,7 +53,7 @@ class Form extends Component { } UNSAFE_componentWillUpdate(nextProps, nextState) { - const { selectedBank, prices } = this.props; + const { selectedBank, prices, getWithdrawCurrency } = this.props; const { activeTab } = this.state; if ( nextProps.selectedBank !== selectedBank || @@ -61,6 +65,9 @@ class Form extends Component { coins, banks, } = this.props; + const currentCurrency = getWithdrawCurrency + ? getWithdrawCurrency + : currency; let initialBank; if (nextState.activeTab === activeTab) { @@ -77,7 +84,7 @@ class Form extends Component { this.generateFormValues( nextState.activeTab, - currency, + currentCurrency, balance, coins, verification_level, @@ -97,6 +104,7 @@ class Form extends Component { user: { balance, verification_level }, coins, prices, + getWithdrawCurrency, } = this.props; const { activeTab } = this.state; @@ -104,10 +112,13 @@ class Form extends Component { if (banks && banks.length === 1) { initialBank = banks[0]['id']; } + const currentCurrency = getWithdrawCurrency + ? getWithdrawCurrency + : currency; this.generateFormValues( activeTab, - currency, + currentCurrency, balance, coins, verification_level, @@ -186,6 +197,7 @@ class Form extends Component { currency, user: { balance }, prices = {}, + withdrawInformation, } = this.props; const { setActiveTab } = this; @@ -199,6 +211,7 @@ class Form extends Component { setActiveTab, activeTab, currentPrice: prices[currency], + withdrawInformation, }; return ( @@ -219,6 +232,7 @@ const mapStateToProps = (state, ownProps) => ({ constants: state.app.constants, banks: withdrawalOptionsSelector(state, ownProps), prices: state.asset.oraclePrices, + getWithdrawCurrency: state.app.withdrawFields.withdrawCurrency, }); const mapDispatchToProps = (dispatch) => ({ diff --git a/web/src/containers/Withdraw/Fiat/WithdrawalForm.js b/web/src/containers/Withdraw/Fiat/WithdrawalForm.js index 249f9bfe0f..af15330887 100644 --- a/web/src/containers/Withdraw/Fiat/WithdrawalForm.js +++ b/web/src/containers/Withdraw/Fiat/WithdrawalForm.js @@ -6,7 +6,7 @@ import { formValueSelector, reset, SubmissionError, - stopSubmit + stopSubmit, } from 'redux-form'; import renderFields from 'components/Form/factoryFields'; import ReviewModalContent from './ReviewModalContent'; @@ -260,21 +260,31 @@ class Index extends Component { router, banks, fiat_fees, + withdrawInformation, + getWithdrawCurrency, } = this.props; const { dialogIsOpen, dialogOtpOpen, successfulRequest } = this.state; const is_verified = id_data.status === 3; const has_verified_bank_account = !!banks.length; - const { icon_id } = coins[currency]; + const { icon_id } = coins[getWithdrawCurrency] || coins[currency]; - const { rate: fee } = getFiatWithdrawalFee(currency); - const customFee = fiat_fees?.[currency]?.withdrawal_fee; + const { rate: fee } = getFiatWithdrawalFee( + currency, + 0, + '', + getWithdrawCurrency + ); + const customFee = getWithdrawCurrency + ? fiat_fees?.[getWithdrawCurrency]?.withdrawal_fee + : fiat_fees?.[currency]?.withdrawal_fee; return (
+ {withdrawInformation} {titleSection} {(!is_verified || !has_verified_bank_account) && ( @@ -363,6 +373,7 @@ const FiatWithdrawalForm = reduxForm({ const mapStateToProps = (state) => ({ data: selector(state, 'bank', 'amount', 'fee'), fiat_fees: state.app.constants.fiat_fees, + getWithdrawCurrency: state.app.withdrawFields.withdrawCurrency, }); export default connect(mapStateToProps)(withRouter(FiatWithdrawalForm)); diff --git a/web/src/containers/Withdraw/Fiat/index.js b/web/src/containers/Withdraw/Fiat/index.js index e7c829ab74..60e170b2a7 100644 --- a/web/src/containers/Withdraw/Fiat/index.js +++ b/web/src/containers/Withdraw/Fiat/index.js @@ -4,11 +4,15 @@ import { SmartTarget, UnderConstruction } from 'components'; import Form from './Form'; const Fiat = ({ ultimate_fiat, ...rest }) => { - const { currency, titleSection } = rest; + const { currency, titleSection, withdrawInformation } = rest; return ( {ultimate_fiat ? ( -
+ ) : ( )} diff --git a/web/src/containers/Withdraw/ReviewModalContent.js b/web/src/containers/Withdraw/ReviewModalContent.js index c0ae455489..3cb29ab86c 100644 --- a/web/src/containers/Withdraw/ReviewModalContent.js +++ b/web/src/containers/Withdraw/ReviewModalContent.js @@ -41,8 +41,9 @@ const ReviewModalContent = ({ onClickCancel, icons: ICONS, hasDestinationTag, + getWithdrawCurrency, }) => { - const { min, fullname, display_name, withdrawal_fees, network } = + const { min, fullname, withdrawal_fees, network } = coins[currency || BASE_CURRENCY] || DEFAULT_COIN_DATA; const baseCoin = coins[BASE_CURRENCY] || DEFAULT_COIN_DATA; const fee_coin = data.fee_coin ? data.fee_coin : ''; @@ -54,6 +55,7 @@ const ReviewModalContent = ({ let min_fee; let max_fee; const feeKey = network ? data.network : currency; + const display_name = getWithdrawCurrency.toUpperCase(); if (withdrawal_fees && withdrawal_fees[feeKey]) { min_fee = withdrawal_fees[feeKey].min; max_fee = withdrawal_fees[feeKey].max; diff --git a/web/src/containers/Withdraw/Withdraw.js b/web/src/containers/Withdraw/Withdraw.js new file mode 100644 index 0000000000..1a850530e4 --- /dev/null +++ b/web/src/containers/Withdraw/Withdraw.js @@ -0,0 +1,1185 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { isMobile } from 'react-device-detect'; +import { Button, Input, Select } from 'antd'; +import BigNumber from 'bignumber.js'; +import { Coin } from 'components'; +import STRINGS from 'config/localizedStrings'; +import { + CaretDownOutlined, + CheckOutlined, + CloseOutlined, + ExclamationCircleFilled, +} from '@ant-design/icons'; +import { STATIC_ICONS } from 'config/icons'; +import { + getWithdrawalMax, + setFee, + setIsValidAdress, + setReceiverEmail, + setSelectedMethod, + setWithdrawOptionaltag, + withdrawAddress, + withdrawAmount, + withdrawCurrency, + withdrawNetwork, + withdrawNetworkOptions, +} from 'actions/appActions'; +import { getPrices } from 'actions/assetActions'; +import { + calculateFee, + calculateFeeCoin, + onHandleSymbol, + renderEstimatedValueAndFee, + renderLabel, + renderNetworkWithLabel, +} from './utils'; +import { email, validAddress } from 'components/Form/validations'; +import strings from 'config/localizedStrings'; + +const RenderWithdraw = ({ + coins, + UpdateCurrency, + onOpenDialog, + assets, + pinnedAssets, + router, + onHandleScan, + selectedNetwork, + ...rest +}) => { + const { Option } = Select; + const methodOptions = [ + strings['WITHDRAW_PAGE.WITHDRAWAL_CONFIRM_ADDRESS'], + strings['FORM_FIELDS.EMAIL_LABEL'], + ]; + const [currStep, setCurrStep] = useState({ + stepOne: false, + stepTwo: false, + stepThree: false, + stepFour: false, + stepFive: false, + }); + const [maxAmount, setMaxAmount] = useState(0); + const [topAssets, setTopAssets] = useState([]); + const [selectedAsset, setSelectedAsset] = useState(null); + const [prices, setPrices] = useState({}); + const [isPinnedAssets, setIsPinnedAssets] = useState(false); + const [optionalTag, setOptionalTag] = useState(''); + const [isValidEmail, setIsValidEmail] = useState(false); + const [isDisbaleWithdraw, setIsDisbaleWithdraw] = useState(false); + // const [isCheck, setIsCheck] = useState(false); + // const [isVisible, setIsVisible] = useState(false); + // const [isWarning, setIsWarning] = useState(false); + + const { + setWithdrawCurrency, + setWithdrawNetworkOptions, + setWithdrawAddress, + setWithdrawAmount, + getWithdrawCurrency, + getWithdrawNetworkOptions, + getWithdrawAddress, + getWithdrawAmount, + setFee, + setWithdrawNetwork, + currency, + coin_customizations, + setIsValidAdress, + isValidAddress, + getNativeCurrency, + selectedMethod, + setSelectedMethod, + setReceiverEmail, + receiverWithdrawalEmail, + setWithdrawOptionaltag, + } = rest; + + const defaultCurrency = currency !== '' && currency; + const iconId = coins[getWithdrawCurrency]?.icon_id; + const coinLength = + coins[getWithdrawCurrency]?.network && + coins[getWithdrawCurrency]?.network.split(','); + let network = + coins[getWithdrawCurrency]?.network && + coins[getWithdrawCurrency]?.network !== 'other' + ? coins[getWithdrawCurrency]?.network + : coins[getWithdrawCurrency]?.symbol; + + const curretPrice = getWithdrawCurrency + ? prices[getWithdrawCurrency] + : prices[defaultCurrency]; + const estimatedWithdrawValue = curretPrice * getWithdrawAmount || 0; + let fee = + selectedMethod === 'Email' + ? 0 + : calculateFee(selectedAsset, getWithdrawNetworkOptions, coins); + const feeCoin = calculateFeeCoin( + selectedAsset, + getWithdrawNetworkOptions, + coins + ); + + const feeMarkup = + selectedAsset && coin_customizations?.[selectedAsset]?.fee_markup; + if (feeMarkup) { + const incrementUnit = coins?.[selectedAsset]?.increment_unit; + const decimalPoint = new BigNumber(incrementUnit).dp(); + const roundedMarkup = new BigNumber(feeMarkup) + .decimalPlaces(decimalPoint) + .toNumber(); + + fee = new BigNumber(fee || 0).plus(roundedMarkup || 0).toNumber(); + } + + const currentNetwork = + getWithdrawNetworkOptions !== '' ? getWithdrawNetworkOptions : network; + const defaultNetwork = + defaultCurrency && + coins[defaultCurrency]?.network && + coins[defaultCurrency]?.network !== 'other' + ? coins[defaultCurrency]?.network + : coins[defaultCurrency]?.symbol; + const isWithdrawal = coins[getWithdrawCurrency]?.allow_withdrawal; + + useEffect(() => { + const topWallet = assets + .filter((item, index) => { + return index <= 3; + }) + .map((data) => { + return data[0]; + }); + if (pinnedAssets.length) { + setTopAssets(pinnedAssets); + } else { + setTopAssets(topWallet); + } + }, [assets, pinnedAssets]); + + useEffect(() => { + UpdateCurrency(getWithdrawCurrency); + setFee(fee); + setWithdrawNetwork(currentNetwork); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getWithdrawCurrency, UpdateCurrency, fee, setFee]); + + useEffect(() => { + if (defaultCurrency) { + setSelectedAsset(defaultCurrency); + setWithdrawCurrency(defaultCurrency); + if (coinLength?.length > 1) { + setCurrStep({ ...currStep, stepTwo: true }); + } + setCurrStep({ ...currStep, stepTwo: true, stepThree: true }); + getWithdrawMAx(defaultCurrency); + } else { + setSelectedAsset(null); + } + // if ( + // ['xrp', 'xlm', 'ton', 'pmn'].includes(defaultCurrency) || + // ['xrp', 'xlm', 'ton', 'pmn'].includes(currentNetwork) + // ) { + // setIsWarning(true); + // } + getOraclePrices(); + setCurrStep({ ...currStep, stepTwo: true }); + + return () => { + setIsValidAdress(false); + setIsValidEmail(false); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (getWithdrawCurrency && !isWithdrawal) { + setSelectedAsset(''); + setIsDisbaleWithdraw(true); + setCurrStep({ + stepOne: true, + stepTwo: true, + stepThree: false, + stepFour: false, + stepFive: false, + }); + } else { + setIsDisbaleWithdraw(false); + } + }, [getWithdrawCurrency, isWithdrawal]); + + const isAmount = useMemo(() => { + const isCondition = + selectedMethod === 'Email' ? !isValidEmail : !isValidAddress; + return ( + !getWithdrawAddress || + !getWithdrawCurrency || + getWithdrawAmount <= 0 || + getWithdrawAmount > maxAmount || + maxAmount <= 0 || + !network || + isCondition + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + getWithdrawAddress, + getWithdrawCurrency, + getWithdrawAmount, + maxAmount, + network, + isValidAddress, + isValidEmail, + ]); + + const getOraclePrices = async () => { + try { + const prices = await getPrices({ coins }); + setPrices(prices); + } catch (error) { + console.error(error); + } + }; + + const getWithdrawMAx = async (getWithdrawCurrency, isMaxAmount = false) => { + try { + const res = await getWithdrawalMax( + getWithdrawCurrency && getWithdrawCurrency, + selectedMethod === 'Email' + ? 'email' + : getWithdrawNetworkOptions + ? getWithdrawNetworkOptions + : network + ? network + : defaultNetwork + ); + isMaxAmount && setWithdrawAmount(res?.data?.amount); + setMaxAmount(res?.data?.amount); + } catch (error) { + console.error(error); + } + }; + + const onHandleChangeMethod = (method) => { + setWithdrawAddress(''); + setWithdrawAmount(''); + setReceiverEmail(''); + setSelectedMethod(method); + setCurrStep((prev) => ({ ...prev, stepTwo: true })); + setIsValidAdress(false); + setIsValidEmail(false); + if (!method) { + setCurrStep((prev) => ({ + ...prev, + stepTwo: false, + stepThree: false, + stepFour: false, + stepFive: false, + })); + } + }; + + const onHandleChangeSelect = (val, pinned_assets = false) => { + if (pinned_assets) { + setIsPinnedAssets(pinned_assets); + } + if (val) { + if ( + currStep.stepTwo || + currStep.stepThree || + currStep.stepFour || + currStep.stepFive + ) { + setCurrStep((prev) => ({ + ...prev, + stepTwo: false, + stepThree: false, + stepFour: false, + stepFive: false, + })); + } + if (coins[val] && !coins[val].allow_withdrawal) { + setCurrStep((prev) => ({ ...prev, stepTwo: true, stepThree: false })); + } else { + setCurrStep((prev) => ({ ...prev, stepTwo: true, stepThree: true })); + } + setWithdrawCurrency(val); + network = val ? val : coins[getWithdrawCurrency]?.symbol; + getWithdrawMAx(val); + setWithdrawNetworkOptions(null); + setIsValidAdress(false); + setIsValidEmail(false); + router.push(`/wallet/${val}/withdraw`); + } else if (!val) { + setWithdrawCurrency(''); + setCurrStep((prev) => ({ + ...prev, + stepThree: false, + stepFour: false, + stepFive: false, + })); + setWithdrawAmount(''); + } + setSelectedAsset( + val && coins[val] && coins[val].allow_withdrawal ? val : '' + ); + setWithdrawAddress(''); + setReceiverEmail(''); + }; + + const renderPinnedAsset = (data) => { + const icon_id = coins[data]?.icon_id; + return ( +
+ {data.toUpperCase()} + + + +
+ ); + }; + + const onHandleChangeNetwork = (val) => { + if (val) { + setCurrStep((prev) => ({ ...prev, stepFour: true })); + setWithdrawNetworkOptions(val); + } else if (!val) { + setCurrStep((prev) => ({ ...prev, stepFour: false, stepFive: false })); + } + }; + + const onHandleAddress = (val, method) => { + const isValid = validAddress( + getWithdrawCurrency, + STRINGS[`WITHDRAWALS_${selectedAsset.toUpperCase()}_INVALID_ADDRESS`], + currentNetwork, + val + )(); + if (method === 'email') { + const validate = email(val); + if (!validate) { + setIsValidEmail(true); + } else { + setIsValidEmail(false); + } + } + if (val) { + setCurrStep((prev) => ({ ...prev, stepFive: true })); + } else if (!val) { + setCurrStep((prev) => ({ ...prev, stepFour: false, stepFive: false })); + } + setWithdrawAddress(val); + setReceiverEmail(val); + setIsValidAdress({ isValid: !isValid }); + setWithdrawAmount(''); + }; + + const onHandleAmount = (val) => { + if (val >= 0) { + setWithdrawAmount(val); + } + }; + + // const onHandleRemove = () => { + // if (!isCheck) { + // setOptionalTag(''); + // } + // setIsCheck(true); + // setIsVisible(false); + // }; + + const renderAmountIcon = () => { + return ( +
getWithdrawMAx(getWithdrawCurrency, true)} + className="d-flex render-amount-icon-wrapper" + > + {renderLabel('CALCULATE_MAX')} +
+ max-icon +
+
+ ); + }; + + // const renderRemoveTag = () => { + // return ( + //
+ //
+ //
+ // + // {STRINGS['WITHDRAW_PAGE.REMOVE_TAG_NOTE_1']} + // + //
+ //
+ // + // {STRINGS.formatString( + // STRINGS['WITHDRAW_PAGE.REMOVE_TAG_NOTE_2'], + // + // {STRINGS['WITHDRAW_PAGE.REMOVE_TAG_NOTE_3']} + // , + // STRINGS['WITHDRAW_PAGE.REMOVE_TAG_NOTE_4'] + // )} + // + //
+ //
+ //
+ // + // + //
+ //
+ // ); + // }; + + // const renderWithdrawWarningPopup = () => { + // return ( + //
+ //
+ // + // {STRINGS['WITHDRAW_PAGE.WARNING_WITHDRAW_INFO_1']} + // + //
+ //
+ // + // {STRINGS['WITHDRAW_PAGE.WARNING_WITHDRAW_INFO_2']} + // + //
+ //
+ // + //
+ //
+ // ); + // }; + + const onHandleOptionalTag = (value) => { + setOptionalTag(value); + setWithdrawOptionaltag(value); + }; + + const onHandleClear = (type) => { + if (type === 'coin') { + setSelectedAsset(null); + setWithdrawCurrency(''); + } + if (type === 'network') { + setWithdrawAddress(null); + setWithdrawNetworkOptions(null); + } + setCurrStep({ + ...currStep, + stepThree: false, + stepFour: false, + stepFive: false, + }); + setIsValidAdress(false); + setIsValidEmail(false); + }; + + // const onHandleSelect = (symbol) => { + // const curr = onHandleSymbol(symbol); + // if (curr !== symbol) { + // if ( + // ['xrp', 'xlm', 'ton'].includes(defaultCurrency) || + // ['xrp', 'xlm', 'ton'].includes(defaultNetwork) + // ) { + // setIsWarning(true); + // } else { + // setIsWarning(false); + // } + // } + // }; + + const renderScanIcon = () => { + return ( +
onHandleScan()} + > + {renderLabel('ACCORDIAN.SCAN')} +
+ scan-icon +
+
+ ); + }; + + const withdrawFeeFormat = + selectedMethod === 'Email' + ? 0 + : `+ ${fee} ${ + (getWithdrawCurrency || currency) && feeCoin?.toUpperCase() + }`; + const estimatedFormat = `≈ ${Math.round( + estimatedWithdrawValue + )} ${getNativeCurrency?.toUpperCase()}`; + const isCondition = + (['xrp', 'xlm'].includes(selectedAsset) || + ['xlm', 'ton'].includes(network)) && + selectedMethod !== 'Email'; + const isEmailAndAddress = + coinLength && coinLength?.length > 1 && selectedMethod !== 'Email' + ? getWithdrawNetworkOptions !== null + : currStep.stepThree || (selectedAsset && selectedMethod); + const renderNetwork = + coinLength && coinLength?.length > 1 && selectedMethod !== 'Email' + ? getWithdrawNetworkOptions + : true; + const renderAmountField = + (selectedMethod === 'Email' && isValidEmail) || + (selectedMethod === 'Address' && isValidAddress); + const isErrorAmountField = getWithdrawAmount > maxAmount && maxAmount > 0; + const networkIcon = selectedNetwork + ? coins[selectedNetwork]?.icon_id + : coins[defaultNetwork]?.icon_id; + const networkOptionsIcon = coins[getWithdrawNetworkOptions]?.icon_id; + + return ( +
+
+
+
+ 1 + +
+
+
+ {renderLabel('WITHDRAWALS_FORM_METHOD')} +
+
+
+ + {currStep.stepTwo && } +
+ {selectedMethod === 'Email' && ( +
+ {renderLabel('WITHDRAWALS_FORM_MAIL_INFO')} +
+ )} +
+
+
+
+
+
+
+ + 2 + + +
+
+
+ {renderLabel('ACCORDIAN.SELECT_ASSET')} +
+
+ {currStep.stepTwo && ( +
+
+ {topAssets.map((data, inx) => { + return ( + onHandleChangeSelect(data, true)} + > + {renderPinnedAsset(data)} + + ); + })} +
+
+ + {selectedMethod === 'Email' ? ( + isEmailAndAddress && renderNetwork ? ( + + ) : ( + + ) + ) : currStep.stepThree || + (selectedAsset && selectedMethod) ? ( + + ) : ( + + )} +
+
+ )} +
+
+
+
+ {selectedMethod !== 'Email' && ( +
+
+
+ + 3 + + +
+
+
+ {renderLabel('ACCORDIAN.SELECT_NETWORK')} +
+ {(currStep.stepThree || (selectedAsset && selectedMethod)) && ( +
+
+ + {selectedMethod !== 'Email' && + isEmailAndAddress && + renderNetwork ? ( + + ) : ( + + )} +
+
+ +
+ {renderLabel('DEPOSIT_FORM_NETWORK_WARNING')} +
+
+
+ )} +
+
+
+ )} +
+
+
+ + {selectedMethod === 'Email' ? 3 : 4} + + +
+
+
+ {renderLabel( + selectedMethod === 'Address' + ? 'ACCORDIAN.DESTINATION' + : 'FORM_FIELDS.EMAIL_LABEL' + )} +
+ {isEmailAndAddress && renderNetwork && ( +
+ {selectedMethod === 'Address' ? ( + onHandleAddress(e.target.value, 'address')} + value={getWithdrawAddress} + placeholder={strings['WITHDRAW_PAGE.WITHDRAW_ADDRESS']} + suffix={renderScanIcon()} + > + ) : ( + onHandleAddress(e.target.value, 'email')} + value={receiverWithdrawalEmail} + placeholder={ + strings['WITHDRAW_PAGE.WITHDRAW_EMAIL_ADDRESS'] + } + > + )} + {selectedMethod === 'Email' ? ( + isValidEmail ? ( + + ) : ( + + ) + ) : isValidAddress ? ( + + ) : ( + + )} +
+ )} +
+
+
+ {isCondition && ( +
+
+
+ + + 5 + + +
+
+
+ {renderLabel('ACCORDIAN.OPTIONAL_TAG')} +
+ {isEmailAndAddress && ( +
+ {/*
+
+ { + !isCheck ? setIsVisible(true) : setIsCheck(!isCheck); + }} + checked={isCheck} + /> + No Tag +
+
*/} +
+ onHandleOptionalTag(e.target.value)} + value={optionalTag} + className="destination-input-field" + type={ + selectedAsset === 'xrp' || selectedAsset === 'xlm' + ? 'number' + : 'text' + } + // disabled={isCheck} + > + {optionalTag && } +
+
+ +
+ {renderLabel('WITHDRAWALS_FORM_DESTINATION_TAG_WARNING')} +
+
+
+ )} +
+
+
+ )} + {/* setIsWarning(false)} + footer={false} + className="withdrawal-remove-tag-modal" + width="420px" + > + {renderWithdrawWarningPopup()} + + setIsVisible(false)} + footer={false} + className="withdrawal-remove-tag-modal" + > + {renderRemoveTag()} + */} +
+
+
+ + {isCondition ? 6 : selectedMethod === 'Email' ? 4 : 5} + + +
+
+
+ + + + + {getWithdrawCurrency.toUpperCase()} + +
+ {renderLabel('ACCORDIAN.AMOUNT')} +
+
+ {renderAmountField && ( +
+
+ onHandleAmount(e.target.value)} + value={getWithdrawAmount} + className={ + isErrorAmountField + ? `destination-input-field field-error` + : `destination-input-field` + } + suffix={renderAmountIcon()} + type="number" + placeholder={strings['WITHDRAW_PAGE.ENTER_AMOUNT']} + > + {!isAmount ? ( + + ) : ( + + )} +
+ {isErrorAmountField && ( +
+ + {renderLabel('WITHDRAW_PAGE.MAX_AMOUNT_WARNING_INFO')} +
+ )} + {!maxAmount && maxAmount === 0 && ( +
+ + {renderLabel('WITHDRAW_PAGE.ZERO_BALANCE')} +
+ )} + {currStep.stepFive && ( +
+
+ +
+
+
+ {renderEstimatedValueAndFee( + renderLabel, + 'ACCORDIAN.ESTIMATED', + estimatedFormat + )} + -- + {renderEstimatedValueAndFee( + renderLabel, + 'ACCORDIAN.TRANSACTION_FEE', + withdrawFeeFormat + )} +
+
+
+ )} + {currStep.stepFive && ( +
+ {isCondition && ( +
+ +
+ )} + {isCondition && ( + + )} +
+ +
+
+ )} +
+ )} +
+
+
+
+ ); +}; + +const mapStateToForm = (state) => ({ + getWithdrawCurrency: state.app.withdrawFields.withdrawCurrency, + getWithdrawNetwork: state.app.withdrawFields.withdrawNetwork, + getWithdrawNetworkOptions: state.app.withdrawFields.withdrawNetworkOptions, + getWithdrawAddress: state.app.withdrawFields.withdrawAddress, + getWithdrawAmount: state.app.withdrawFields.withdrawAmount, + coin_customizations: state.app.constants.coin_customizations, + isValidAddress: state.app.isValidAddress, + getNativeCurrency: state.app.constants.native_currency, + selectedMethod: state.app.selectedWithdrawMethod, + receiverWithdrawalEmail: state.app.receiverWithdrawalEmail, +}); + +const mapDispatchToProps = (dispatch) => ({ + setWithdrawCurrency: bindActionCreators(withdrawCurrency, dispatch), + setWithdrawNetwork: bindActionCreators(withdrawNetwork, dispatch), + setWithdrawNetworkOptions: bindActionCreators( + withdrawNetworkOptions, + dispatch + ), + setWithdrawAddress: bindActionCreators(withdrawAddress, dispatch), + setWithdrawAmount: bindActionCreators(withdrawAmount, dispatch), + setFee: bindActionCreators(setFee, dispatch), + setIsValidAdress: bindActionCreators(setIsValidAdress, dispatch), + setSelectedMethod: bindActionCreators(setSelectedMethod, dispatch), + setReceiverEmail: bindActionCreators(setReceiverEmail, dispatch), + setWithdrawOptionaltag: bindActionCreators(setWithdrawOptionaltag, dispatch), +}); + +export default connect(mapStateToForm, mapDispatchToProps)(RenderWithdraw); diff --git a/web/src/containers/Withdraw/_Withdraw.scss b/web/src/containers/Withdraw/_Withdraw.scss index 4fd1930dab..120907f831 100644 --- a/web/src/containers/Withdraw/_Withdraw.scss +++ b/web/src/containers/Withdraw/_Withdraw.scss @@ -7,6 +7,7 @@ $content--margin: 2rem; .review-icon { @include size(8rem); margin: 0 auto; + svg { @include size(8rem); } @@ -20,6 +21,7 @@ $content--margin: 2rem; font-size: $font-size-subhead2; border-bottom: 1px solid $colors-main-black; text-align: center; + .review-fee_message { font-size: $font-size-main; } @@ -30,6 +32,7 @@ $content--margin: 2rem; width: 1px; height: 3rem; background-color: $arrow-color; + &:after { content: ''; position: absolute; @@ -76,30 +79,429 @@ $content--margin: 2rem; } .withdrawal-container { + width: 75rem !important; + + .transaction-history-wrapper { + max-width: 100% !important; + } + .icon_title-wrapper { flex-direction: row; justify-content: flex-start; } + + .icon_title-wrapper .icon_title-text { + font-size: 2.5rem; + } + + .withdraw-icon { + .icon_title-svg { + width: unset !important; + height: unset !important; + + svg { + width: 3rem !important; + height: 3rem !important; + } + } + } + .withdraw-form-wrapper { .withdraw-form { border-radius: 4px; margin: 0 0 40px; background-color: $app-sidebar-background; - padding: 30px; + padding: 30px 30px 30px 60px; & .field-children .field-valid { top: 8px; right: -20px; } + + .custom-select-input-style { + width: 80%; + } + + .withdraw-deposit-icon-wrapper { + div { + justify-content: unset; + align-items: unset; + } + } + + .withdraw-deposit-disable { + opacity: 0.5; + } + + .withdraw-deposit-icon { + width: 100%; + height: 2.5rem; + border: 2px solid $colors-main-black; + border-radius: 100%; + padding: 2px; + } + + .withdraw-deposit-content { + font-size: 14px; + padding: 1% 1%; + } + + .destination-input-field { + width: 80%; + height: 38px; + padding: 10px 10px; + border-radius: 2px; + border: 1px solid var(--calculated_important-border); + background-color: var(--base_wallet-sidebar-and-popup); + color: $colors-main-black; + + input { + background-color: var(--base_wallet-sidebar-and-popup); + color: $colors-main-black; + text-overflow: ellipsis; + } + } + + .destination-input-field:hover { + border: 1px solid $colors-main-black; + } + + .field-error { + border: 2px solid $colors-notifications-red; + } + + .field-error:hover { + border: 2px solid $colors-notifications-red; + } + + .withdraw-main-label { + color: $colors-black; + font-size: 14px; + } + + .withdraw-main-label-selected { + color: $colors-main-black; + text-wrap: nowrap; + } + + .side-icon-wrapper { + display: flex; + width: 15% !important; + height: 25%; + + svg { + width: 100%; + opacity: 0.1; + } + } + + .custom-field { + .custom-step, + .custom-step-selected { + border: 2px solid $colors-black; + border-radius: 100%; + width: fit-content; + padding: 3px 10px; + } + + .custom-step-selected { + border: 2px solid $colors-main-black; + } + + .custom-line, + .custom-line-selected, + .custom-line-selected-large { + border: 1px solid $colors-black; + height: 60px; + margin: 0 100%; + } + + .custom-line-selected { + height: 70px; + border: 1px solid $colors-main-black; + } + + .custom-line-selected-large { + height: 70px; + border: 1px solid $colors-main-black; + } + + .custom-line-extra-large { + height: 2rem; + border: 1px solid $colors-black; + margin: 0 100%; + } + + .custom-line-extra-large-active { + border: 1px solid $colors-main-black !important; + } + } + + .select-label { + text-wrap: nowrap; + } + + .amount-field-icon { + margin-top: 1%; + } + + .select-wrapper { + display: flex; + flex-direction: column; + width: 65%; + + .select-field:hover { + border: 1px solid $colors-main-black; + border-radius: 0.35rem; + } + + .disabled { + .ant-select-show-search.ant-select-single:not(.ant-select-customize-input) + .ant-select-selector { + cursor: not-allowed !important; + } + } + + .ant-select-show-search.ant-select-single:not(.ant-select-customize-input) + .ant-select-selector { + cursor: pointer !important; + } + + .custom-select-input-style .ant-select-selection-search-input { + height: 100% !important; + } + + .ant-select-single + .ant-select-selector + .ant-select-selection-placeholder { + margin-top: 1%; + pointer-events: unset !important; + } + + .custom-select-input-style .ant-select-selection-item { + line-height: 40px !important; + } + + .custom-select-input-style .ant-select-selector { + height: 40px !important; + } + } + + .withdraw-deposit-disable .select-field:hover { + border: $colors-black; + } + + .opacity-100 { + opacity: 1; + } + + .opacity-50 { + opacity: 0.5; + } + + .opacity-30 { + opacity: 0.3; + } + + .ant-select-clear { + user-select: none; + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + background-color: $colors-main-black; + + svg { + color: $app-sidebar-background; + font-size: 18px; + } + } + + .ant-input-suffix { + width: 5rem; + } + + .render-amount-icon-wrapper { + color: var(--specials_buttons-links-and-highlights); + cursor: pointer; + + .suffix-text { + padding-right: 0.5rem; + text-transform: uppercase; + font-size: 15px; + + .edit-wrapper__container { + text-decoration: underline; + } + } + + .img-wrapper { + img { + height: 1.5rem; + } + } + } + + .currency-label { + border: 1px solid $colors-black; + padding: 0.5%; + border-radius: 5px; + margin-right: 10px; + width: 5rem; + text-align: center; + cursor: pointer; + .pinned-asset-icon { + padding-top: 3%; + } + } + + .render-scan-wrapper { + color: var(--specials_buttons-links-and-highlights); + cursor: pointer; + + .suffix-text { + padding-right: 0.5rem; + font-size: 15px; + + .edit-wrapper__container { + text-decoration: underline; + } + } + + .img-wrapper { + display: flex; + align-items: center; + + img { + height: 1rem; + } + } + } + + .destination-field { + height: 60px; + } + + .destination-field-mobile { + height: 6rem; + } } + + .bottom-content { + margin-top: 3%; + + .fee-fields { + color: $colors-black; + } + } + .btn-wrapper { display: flex; justify-content: center; - align-items: center; + .holla-button { width: 300px; } } + + .withdraw-btn-wrapper { + display: flex; + margin-top: 10%; + width: 80%; + + .ant-btn[disabled], + .ant-btn[disabled]:hover, + .ant-btn[disabled]:focus, + .ant-btn[disabled]:active, + .ant-btn { + background: $colors-deactivate-color1; + border: unset; + width: 300px; + font-family: 'Raleway'; + color: $app-sidebar-background; + font-size: 1.1rem; + } + + .ant-btn { + padding: 0 4px; + background: $colors-notifications-blue; + color: $colors-main-black; + } + + .holla-button { + width: 300px; + } + } + + .warning-text, + .email-text { + color: $colors-notification-pending; + font-size: 12px; + width: 90%; + } + + .email-text { + color: $colors-black !important; + } + + .width-80 { + width: 80%; + } + + .check-optional .ant-checkbox-inner { + background-color: var(--base_wallet-sidebar-and-popup) !important; + } + + .check-optional { + .ant-checkbox-checked .ant-checkbox-inner { + border-color: $colors-main-black !important; + } + } + } +} + +.withdrawal-remove-tag-modal { + .ant-modal-header { + border: none; + } + + .ant-modal-header, + .ant-modal-body { + background-color: var(--base_wallet-sidebar-and-popup) !important; + } + + .ant-modal-body { + padding-top: 0 !important; + } + + .remove-tag-wrapper, + .warning-popup-wrapper { + color: $colors-main-black; + + .tag-body { + height: 18rem; + } + + .button-wrapper { + display: flex; + justify-content: space-between; + + Button { + width: 48%; + height: 3rem; + border: none !important; + color: $colors-main-black !important; + font-size: 14px; + } + } + } + + .warning-popup-wrapper { + .button-wrapper { + margin-top: 5%; + Button { + width: 100%; + } + } } } @@ -108,6 +510,7 @@ $content--margin: 2rem; min-width: 0; margin: auto; } + .presentation_container { .action_notification-svg { svg { @@ -115,16 +518,20 @@ $content--margin: 2rem; height: 1.5rem !important; } } + .inner_container { .result-message { font-size: $regular--font-size; } + .deposit_info-wrapper { .deposit_info-crypto-wrapper { width: 100%; + .field-label { font-size: $title--font-size; } + .field-children { .custom { font-size: $extra-thick--font-size; @@ -132,32 +539,39 @@ $content--margin: 2rem; } } } + .qr_code-wrapper { width: 24rem; max-width: 24rem; + .qr-code-bg > *:first-child { width: 24rem !important; height: 24rem !important; } + .qr-text { font-size: $font-size-mobile-innertxt !important; } } + .information_block { .information_block-text_wrapper { .title { font-size: $font-size-mobile-title; } + .text { font-size: $font-size-mobile-txt !important; } } } + .field-label, .field-children, .input_field-input { font-size: $font-size-mobile-txt !important; } + .field-error-content { font-size: $regular--font-size; } @@ -168,34 +582,131 @@ $content--margin: 2rem; line-height: $regular--font-size; } } + .holla-button { font-size: $font-size-mobile-txt !important; } } } + + .withdrawal-remove-tag-modal { + .remove-tag-wrapper { + .button-wrapper { + Button { + height: 4rem; + font-size: 12px !important; + margin-top: 3%; + } + } + } + } + .review_email-wrapper { margin: 0px auto !important; } .withdrawal-container { + padding-top: 0px !important; .withdraw-form-wrapper { .withdraw-form { + margin: 0 0 30px; + .destination-field { + height: 75px; + } + + .destination-field-mobile { + height: 11rem; + } + .field-wrapper { .field-label-wrapper { flex-direction: column; } } + + padding: 5%; + + .withdraw-main-label-selected { + font-size: 14px; + } + + .withdraw-deposit-content { + padding: 0 1%; + } + + .custom-line-selected-large { + height: 13rem; + border: 1px solid $colors-main-black; + } + + .custom-field { + .custom-line-selected { + height: 100%; + } + + .custom-line { + height: 13rem; + } + + .custom-line-network-selected { + height: 15rem; + } + + .custom-line-selected-mobile { + height: 9rem; + } + } + + .mobile-view { + margin-top: 5%; + } + + .ant-select-clear svg { + font-size: 18px; + } + + .select-wrapper { + width: 100%; + margin-left: 5%; + } + + .bottom-content { + font-size: 14px; + margin-top: 3%; + } + + .withdraw-main-label-selected, + .withdraw-main-label, + .warning-text { + font-size: 14px; + } + + .currency-label { + width: 15%; + font-size: 14px; + } + + .render-scan-wrapper { + .suffix-text { + font-size: 12px; + } + } } } } + @media screen and (max-width: 550px) { .presentation_container { .inner_container { + .withdraw-icon { + margin-top: 0px !important; + } .information_block { .information_block-text_wrapper { .title { font-size: 14px !important; } + .text { font-size: 12px !important; } @@ -203,23 +714,72 @@ $content--margin: 2rem; } } } - } - @media screen and (max-width: 390px) { - .presentation_container { - .inner_container { - .information_block { - .information_block-text_wrapper { - .title { - font-size: 13px !important; + .withdrawal-container { + .withdraw-form-wrapper { + .withdraw-form { + margin: 0 0 20px; + .select-wrapper { + margin-left: 7%; + } + + .ant-select-clear svg { + font-size: 12px; + } + + .custom-field { + .custom-step-selected, + .custom-step { + padding: 33% 70% 10%; } - .text { - font-size: 10px !important; + } + + .custom-select-input-style { + .ant-select-selector { + font-size: 1.5rem !important; + } + + .ant-select-selection-item { + margin-top: 4%; + line-height: unset !important; } } + + .currency-label { + font-size: 12px; + } + + .mobile-view { + margin-top: 3%; + } + + .amount-field-icon { + margin-top: 2%; + } + + .warning-text { + font-size: 10px; + } + + .custom-line-selected-large { + height: 17rem; + } } - .holla-button { - font-size: 12px !important; + } + } + } + + @media screen and (max-width: 390px) { + .withdrawal-container { + .withdraw-form-wrapper { + .withdraw-form { + .custom-line-selected-large { + height: 18rem; + } + + .withdraw-btn-wrapper { + margin-top: 20%; + } } } } @@ -235,32 +795,6 @@ $content--margin: 2rem; } } -@media screen and (max-width: 1096px) { - .withdrawal-container { - .withdraw-form-wrapper { - .withdraw-form { - .field-wrapper { - .field-label { - font-size: $font-size-mobile-innertxt; - } - .input-box-field { - .input_field-input { - &::placeholder { - font-size: $font-size-mobile-innertxt; - } - } - } - } - } - .btn-wrapper { - .holla-button { - width: 200px; - } - } - } - } -} - .qr_reader_wrapper { width: 25rem; margin: 2rem; diff --git a/web/src/containers/Withdraw/form.js b/web/src/containers/Withdraw/form.js index 71a9f5e793..003f8ac90f 100644 --- a/web/src/containers/Withdraw/form.js +++ b/web/src/containers/Withdraw/form.js @@ -5,24 +5,33 @@ import { formValueSelector, reset, SubmissionError, - stopSubmit + stopSubmit, } from 'redux-form'; +import { bindActionCreators } from 'redux'; import math from 'mathjs'; -import { Button, Dialog, OtpForm, Loader, SmartTarget } from 'components'; -import renderFields from 'components/Form/factoryFields'; +import { Dialog, OtpForm, Loader, SmartTarget } from 'components'; import { setWithdrawEmailConfirmation, setWithdrawNotificationError, } from './notifications'; import { BASE_CURRENCY } from 'config/constants'; -import { calculateBaseFee } from './utils'; +import { + calculateBaseFee, + calculateFeeCoin, + generateBaseInformation, + renderLabel, +} from './utils'; +import { setWithdrawOptionaltag, withdrawCurrency } from 'actions/appActions'; +import { renderInformation } from 'containers/Wallet/components'; +import { assetsSelector } from 'containers/Wallet/utils'; import Fiat from './Fiat'; import Image from 'components/Image'; import STRINGS from 'config/localizedStrings'; -import { message } from 'antd'; -import { getWithdrawalMax } from 'actions/appActions'; import ReviewModalContent from './ReviewModalContent'; import QRScanner from './QRScanner'; +import TransactionsHistory from 'containers/TransactionsHistory'; +import RenderWithdraw from './Withdraw'; +import { isMobile } from 'react-device-detect'; export const FORM_NAME = 'WithdrawCryptocurrencyForm'; @@ -50,6 +59,8 @@ class Form extends Component { dialogOtpOpen: false, otp_code: '', prevFee: null, + currency: '', + renderFiat: false, }; UNSAFE_componentWillReceiveProps(nextProps) { @@ -88,50 +99,94 @@ class Form extends Component { } componentWillUnmount() { + const { setWithdrawCurrency } = this.props; if (errorTimeOut) { clearTimeout(errorTimeOut); } + setWithdrawCurrency(''); } onOpenDialog = (ev) => { - if (ev && ev.preventDefault) { - ev.preventDefault(); - } - const emailMethod = this.props?.data?.method === 'email'; - getWithdrawalMax( - this.props.currency, - !emailMethod ? this.props?.data?.network : 'email' - ) - .then((res) => { - if (math.larger(this.props?.data?.amount, res?.data?.amount)) { - message.error( - `requested amount exceeds maximum withrawal limit of ${ - res?.data?.amount - } ${this?.props?.currency?.toUpperCase()}` - ); - } else { - this.setState({ dialogIsOpen: true }); - } - }) - .catch((err) => { - message.error(err.response.data.message); - }); + // if (ev && ev.preventDefault) { + // ev.preventDefault(); + // } + // const emailMethod = this.props?.data?.method === 'email'; + // const currentCurrency = coins[getWithdrawCurrency]?.symbol || currency; + // const network = getWithdrawNetworkOptions ? getWithdrawNetworkOptions : getWithdrawNetwork ? getWithdrawNetwork : !emailMethod ? this.props?.data?.network : 'email' + // const amount = getWithdrawAmount ? getWithdrawAmount : this.props?.data?.amount + // getWithdrawalMax( + // currentCurrency, + // network + // ) + // .then((res) => { + // if (math.larger(amount, res?.data?.amount)) { + // message.error( + // `requested amount exceeds maximum withrawal limit of ${res?.data?.amount + // } ${currentCurrency.toUpperCase()}` + // ); + // } else { + // this.setState({ dialogIsOpen: true }); + // } + // }) + // .catch((err) => { + // message.error(err?.response?.data?.message); + // }); + + this.setState({ dialogIsOpen: true }); }; onCloseDialog = (ev) => { + const { setWithdrawOptionaltag } = this.props; if (ev && ev.preventDefault) { ev.preventDefault(); } this.setState({ dialogIsOpen: false, dialogOtpOpen: false }); + setWithdrawOptionaltag(''); }; onAcceptDialog = () => { + const { + data, + email, + getWithdrawNetworkOptions, + getWithdrawNetwork, + getWithdrawAmount, + getWithdrawAddress, + getWithdrawCurrency, + currency, + coins, + optionalTag, + } = this.props; + const currentCurrency = getWithdrawCurrency + ? getWithdrawCurrency + : currency; + const network = getWithdrawNetworkOptions + ? getWithdrawNetworkOptions + : getWithdrawNetwork; + const defaultNetwork = + currentCurrency && + coins[currentCurrency]?.network && + coins[currentCurrency]?.network !== 'other' + ? coins[currentCurrency]?.network + : coins[currentCurrency]?.symbol; if (this.props.otp_enabled) { this.setState({ dialogOtpOpen: true }); } else { this.onCloseDialog(); // this.props.submit(); - const values = { ...this.props.data, email: this.props.email }; + let values = { + ...data, + email: email, + amount: getWithdrawAmount, + address: optionalTag + ? `${getWithdrawAddress}:${optionalTag}` + : getWithdrawAddress, + fee_coin: currentCurrency, + network: network ? network : defaultNetwork, + }; + if (!coins[currentCurrency]?.network) { + delete values.network; + } return this.props .onSubmitWithdrawReq({ ...values, @@ -139,7 +194,7 @@ class Form extends Component { }) .then((response) => { this.props.onSubmitSuccess( - { ...response.data, currency: this.props.currency }, + { ...response.data, currency: currentCurrency }, this.props.dispatch ); return response; @@ -160,7 +215,54 @@ class Form extends Component { }; onSubmitOtp = ({ otp_code = '' }) => { - const values = this.props.data; + const { + data, + coins, + currency, + getWithdrawCurrency, + getWithdrawAmount, + getWithdrawAddress, + selectedMethod, + receiverWithdrawalEmail, + getWithdrawNetworkOptions, + getWithdrawNetwork, + optionalTag, + } = this.props; + const network = getWithdrawNetworkOptions + ? getWithdrawNetworkOptions + : getWithdrawNetwork; + const currentCurrency = getWithdrawCurrency + ? getWithdrawCurrency + : currency; + const defaultNetwork = + currentCurrency && + coins[currentCurrency]?.network && + coins[currentCurrency]?.network !== 'other' + ? coins[currentCurrency]?.network + : coins[currentCurrency]?.symbol; + let values = { ...data }; + if (selectedMethod === 'Email') { + values = { + ...data, + email: receiverWithdrawalEmail, + amount: getWithdrawAmount, + address: '', + method: 'email', + network: network ? network : defaultNetwork, + }; + } else { + values = { + ...data, + amount: getWithdrawAmount, + address: optionalTag + ? `${getWithdrawAddress}:${optionalTag}` + : getWithdrawAddress, + network: network ? network : defaultNetwork, + }; + } + if (!coins[currentCurrency]?.network) { + delete values.network; + } return this.props .onSubmitWithdrawReq({ ...values, @@ -194,46 +296,101 @@ class Form extends Component { }); }; + UpdateCurrency = (currency) => { + this.setState({ currency }); + }; + render() { const { submitting, - pristine, error, - valid, - currency, data, openContactForm, - formValues, currentPrice, coins, titleSection, icons: ICONS, selectedNetwork, targets, - email, qrScannerOpen, closeQRScanner, getQRData, + balance, + links, + orders, + pinnedAssets, + assets, + currency, + getWithdrawAmount, + getWithdrawAddress, + getWithdrawCurrency, + getWithdrawNetworkOptions, + getWithdrawNetwork, + getFee, + isFiat, + selectedMethod, + receiverWithdrawalEmail, + optionalTag, + router, + onHandleScan, } = this.props; - const formData = { ...data, email }; + const currentNetwork = getWithdrawNetwork + ? getWithdrawNetwork + : getWithdrawNetworkOptions; + const feeCoin = calculateFeeCoin( + currency, + getWithdrawNetworkOptions, + coins + ); + + const formData = { + ...data, + fee: selectedMethod === 'Email' ? 0 : getFee, + amount: getWithdrawAmount, + destination_tag: optionalTag && optionalTag, + address: + selectedMethod === 'Email' + ? '' + : optionalTag + ? `${getWithdrawAddress}:${optionalTag}` + : getWithdrawAddress, + network: selectedMethod === 'Email' ? 'email' : currentNetwork, + fee_coin: feeCoin, + method: selectedMethod === 'Email' ? 'email' : 'address', + email: selectedMethod === 'Email' ? receiverWithdrawalEmail : null, + }; + const coinObject = coins[getWithdrawCurrency] || coins[currency]; const { dialogIsOpen, dialogOtpOpen } = this.state; const hasDestinationTag = currency === 'xrp' || currency === 'xlm' || selectedNetwork === 'xlm' || selectedNetwork === 'ton'; - - const coinObject = coins[currency]; - const GENERAL_ID = 'REMOTE_COMPONENT__FIAT_WALLET_WITHDRAW'; const currencySpecificId = `${GENERAL_ID}__${currency.toUpperCase()}`; const id = targets.includes(currencySpecificId) ? currencySpecificId : GENERAL_ID; + const currentCurrency = getWithdrawCurrency + ? getWithdrawCurrency + : currency; - if (coinObject && coinObject.type !== 'fiat') { + const withdrawInformation = renderInformation( + currentCurrency, + balance, + false, + generateBaseInformation, + coins, + 'withdraw', + links, + ICONS['BLUE_QUESTION'], + 'BLUE_QUESTION', + orders + ); + + if ((coinObject && coinObject.type !== 'fiat') || !coinObject) { return ( -
-
- +
+ {!coinObject?.allow_withdrawal && this.state.currency && ( +
+
+ +
+ + {renderLabel('ACCORDIAN.DISABLED_WITHDRAW_CONTENT')} + +
+ )} + {this.state.currency && coinObject?.allow_withdrawal && ( +
+ + {withdrawInformation} +
+ )} + - {titleSection} + {!error &&
{error}
}
- {renderFields(formValues)} - {error &&
{error}
} -
-
-
) : !submitting ? ( ) : ( @@ -302,10 +492,18 @@ class Form extends Component { )} + ); } else if (coinObject && coinObject.type === 'fiat') { - return ; + return ( + + ); } else { return
{STRINGS['DEPOSIT.NO_DATA']}
; } @@ -339,8 +537,28 @@ const mapStateToForm = (state) => ({ coins: state.app.coins, targets: state.app.targets, balance: state.user.balance, + pinnedAssets: state.app.pinned_assets, + assets: assetsSelector(state), + getWithdrawCurrency: state.app.withdrawFields.withdrawCurrency, + getWithdrawNetwork: state.app.withdrawFields.withdrawNetwork, + getWithdrawNetworkOptions: state.app.withdrawFields.withdrawNetworkOptions, + getWithdrawAddress: state.app.withdrawFields.withdrawAddress, + getWithdrawAmount: state.app.withdrawFields.withdrawAmount, + optionalTag: state.app.withdrawFields.optionalTag, + getFee: state.app.withdrawFields.withdrawFee, + isValidAddress: state.app.isValidAddress, + selectedMethod: state.app.selectedWithdrawMethod, + receiverWithdrawalEmail: state.app.receiverWithdrawalEmail, +}); + +const mapDispatchToProps = (dispatch) => ({ + setWithdrawCurrency: bindActionCreators(withdrawCurrency, dispatch), + setWithdrawOptionaltag: bindActionCreators(setWithdrawOptionaltag, dispatch), }); -const WithdrawFormWithValues = connect(mapStateToForm)(WithdrawForm); +const WithdrawFormWithValues = connect( + mapStateToForm, + mapDispatchToProps +)(WithdrawForm); export default WithdrawFormWithValues; diff --git a/web/src/containers/Withdraw/index.js b/web/src/containers/Withdraw/index.js index b1ef52a419..d49fe982e3 100644 --- a/web/src/containers/Withdraw/index.js +++ b/web/src/containers/Withdraw/index.js @@ -6,30 +6,25 @@ import { isMobile } from 'react-device-detect'; import math from 'mathjs'; import { message } from 'antd'; -import { Loader, MobileBarBack } from 'components'; +import WithdrawCryptocurrency from './form'; +import strings from 'config/localizedStrings'; import withConfig from 'components/ConfigProvider/withConfig'; +import { Loader, MobileBarBack } from 'components'; import { getCurrencyFromName } from 'utils/currency'; -import { - performWithdraw, - // requestWithdrawFee -} from 'actions/walletActions'; +import { performWithdraw } from 'actions/walletActions'; import { errorHandler } from 'components/OtpForm/utils'; - -import { openContactForm, getWithdrawalMax } from 'actions/appActions'; - -import WithdrawCryptocurrency from './form'; -import { generateFormValues, generateInitialValues } from './formUtils'; -import { generateBaseInformation } from './utils'; - import { - renderInformation, - renderTitleSection, - renderNeedHelpAction, -} from '../Wallet/components'; - + openContactForm, + getWithdrawalMax, + withdrawAddress, + setReceiverEmail, +} from 'actions/appActions'; +import { generateFormValues, generateInitialValues } from './formUtils'; +import { renderDeposit, renderTitleSection } from '../Wallet/components'; import { FORM_NAME } from './form'; import { STATIC_ICONS } from 'config/icons'; import { renderBackToWallet } from 'containers/Deposit/utils'; +import { IconTitle } from 'hollaex-web-lib'; class Withdraw extends Component { state = { @@ -50,8 +45,11 @@ class Withdraw extends Component { UNSAFE_componentWillReceiveProps(nextProps) { if (!this.state.checked) { - if (nextProps.verification_level) { - this.validateRoute(nextProps.routeParams.currency, nextProps.coins); + if ( + nextProps.verification_level && + nextProps.verification_level !== this.props.verification_level + ) { + this.validateRoute(nextProps.routeParams.currency, this.props.coins); } } else if ( nextProps.activeLanguage !== this.props.activeLanguage || @@ -85,8 +83,20 @@ class Withdraw extends Component { } } + componentWillUnmount() { + this.props.setWithdrawAddress(''); + this.props.setReceiverEmail(''); + } + validateRoute = (currency, coins) => { - if (!coins[currency]) { + const { getWithdrawCurrency } = this.props; + if ( + (this.props.isDepositAndWithdraw || + this.props.route.path === 'wallet/withdraw') && + !getWithdrawCurrency + ) { + this.props.router.push('/wallet/withdraw'); + } else if (!coins[currency]) { this.props.router.push('/wallet'); } else if (currency) { this.setState({ checked: true }); @@ -94,8 +104,9 @@ class Withdraw extends Component { }; setCurrency = (currencyName) => { + const { getWithdrawCurrency } = this.props; const currency = getCurrencyFromName(currencyName, this.props.coins); - if (currency) { + if (currency || getWithdrawCurrency) { const { coins } = this.props; const coin = coins[currency]; const networks = coin.network && coin.network.split(','); @@ -127,6 +138,11 @@ class Withdraw extends Component { // if (currency === 'btc' || currency === 'bch' || currency === 'eth') { // this.props.requestWithdrawFee(currency); // } + } else if ( + this.props.isDepositAndWithdraw || + this.props.route.path === 'wallet/withdraw' + ) { + this.props.router.push('/wallet/withdraw'); } else { this.props.router.push('/wallet'); } @@ -186,6 +202,11 @@ class Withdraw extends Component { onSubmitWithdraw = (currency) => (values) => { const { destination_tag, network, ...rest } = values; + const { getWithdrawCurrency, selectedWithdrawMethod } = this.props; + + const currentCurrency = getWithdrawCurrency + ? getWithdrawCurrency + : this.state.currency; let address = rest.address.trim(); if (destination_tag) address = `${rest.address.trim()}:${destination_tag}`; @@ -195,7 +216,9 @@ class Withdraw extends Component { ...rest, address, amount: math.eval(values.amount), - currency, + currency: currentCurrency, + method: selectedWithdrawMethod === 'Email' ? 'email' : 'address', + network: selectedWithdrawMethod === 'Email' ? 'email' : network, }; delete paramData.fee_type; @@ -210,7 +233,7 @@ class Withdraw extends Component { delete paramData.email; } - return performWithdraw(currency, paramData) + return performWithdraw(currentCurrency, paramData) .then((response) => { return { ...response.data, currency: this.state.currency }; }) @@ -261,6 +284,14 @@ class Withdraw extends Component { this.props.router.push('/wallet'); }; + onHandleDeposit = () => { + this.props.router.push('/wallet/deposit'); + }; + + onHandleScan = () => { + this.setState({ qrScannerOpen: true }); + }; + render() { const { balance, @@ -269,11 +300,13 @@ class Withdraw extends Component { openContactForm, activeLanguage, router, - coins, icons: ICONS, selectedNetwork, email, orders, + coins, + getWithdrawCurrency, + isDepositAndWithdraw, } = this.props; const { links = {} } = this.props.constants; const { @@ -284,16 +317,26 @@ class Withdraw extends Component { selectedMethodData, qrScannerOpen, } = this.state; - if (!currency || !checked) { + if ( + (!currency || !checked) && + !this.props.isDepositAndWithdraw && + this.props.route.path !== 'wallet/withdraw' + ) { return
; } const balanceAvailable = balance[`${currency}_available`]; - if (balanceAvailable === undefined) { + if ( + balanceAvailable === undefined && + !this.props.isDepositAndWithdraw && + this.props.route.path !== 'wallet/withdraw' + ) { return ; } + const isFiat = coins[getWithdrawCurrency]?.type === 'fiat'; + const formProps = { currency, onSubmitWithdrawReq: this.onSubmitWithdraw(currency), @@ -313,6 +356,12 @@ class Withdraw extends Component { closeQRScanner: this.closeQRScanner, qrScannerOpen, getQRData: this.getQRData, + balance, + links, + orders, + isFiat, + isDepositAndWithdraw, + onHandleScan: this.onHandleScan, }; return ( @@ -322,8 +371,9 @@ class Withdraw extends Component { )}
{!isMobile && + isFiat && renderTitleSection( - currency, + getWithdrawCurrency, 'withdraw', ICONS['WITHDRAW'], coins, @@ -333,35 +383,25 @@ class Withdraw extends Component { {verification_level >= MIN_VERIFICATION_LEVEL_TO_WITHDRAW && verification_level <= MAX_VERIFICATION_LEVEL_TO_WITHDRAW ? ( */}
-
-
- {renderBackToWallet()} - {openContactForm && - renderNeedHelpAction( - openContactForm, - links, - ICONS['BLUE_QUESTION'], - 'BLUE_QUESTION' - )} + )} +
+
+ {renderBackToWallet(this.onGoBack)} + {openContactForm && renderDeposit(this.onHandleDeposit)}
- + {/* {renderExtraInformation(currency, bank_account, ICONS["BLUE_QUESTION"])} */}
{/* // This commented code can be used if you want to enforce user to have a verified bank account before doing the withdrawal @@ -397,10 +437,16 @@ const mapStateToProps = (store) => ({ config_level: store.app.config_level, orders: store.order.activeOrders, coin_customizations: store.app.constants.coin_customizations, + getWithdrawCurrency: store.app.withdrawFields.withdrawCurrency, + getWithdrawNetwork: store.app.withdrawFields.withdrawNetwork, + isDepositAndWithdraw: store.app.depositAndWithdraw, + selectedWithdrawMethod: store.app.selectedWithdrawMethod, }); const mapDispatchToProps = (dispatch) => ({ openContactForm: bindActionCreators(openContactForm, dispatch), + setWithdrawAddress: bindActionCreators(withdrawAddress, dispatch), + setReceiverEmail: bindActionCreators(setReceiverEmail, dispatch), // requestWithdrawFee: bindActionCreators(requestWithdrawFee, dispatch), dispatch, }); diff --git a/web/src/containers/Withdraw/utils.js b/web/src/containers/Withdraw/utils.js index c007c0f1f3..94d1076396 100644 --- a/web/src/containers/Withdraw/utils.js +++ b/web/src/containers/Withdraw/utils.js @@ -1,6 +1,6 @@ import React from 'react'; import mathjs from 'mathjs'; -import { Accordion } from 'components'; +import { Accordion, Coin, EditWrapper } from 'components'; import { BANK_WITHDRAWAL_BASE_FEE, BANK_WITHDRAWAL_DYNAMIC_FEE_RATE, @@ -11,6 +11,7 @@ import { import STRINGS from 'config/localizedStrings'; import { renderBankInformation } from '../Wallet/components'; +import { getNetworkNameByKey } from 'utils/wallet'; export const generateBaseInformation = (currency, limits = {}) => { const { minAmount = 2, maxAmount = 10000 } = limits; @@ -75,3 +76,84 @@ export const calculateBaseFee = (amount = 0) => { const fee = mathjs.ceil(withdrawalFee.done()); return fee; }; + +export const renderLabel = (label) => { + return {STRINGS[label]}; +}; + +export const renderEstimatedValueAndFee = ( + renderWithdrawlabel, + label, + format +) => { + return ( +
+
{renderWithdrawlabel(label)}
+
{format}
+
+ ); +}; + +export const calculateFee = ( + selectedAsset, + getWithdrawNetworkOptions, + coins +) => { + return selectedAsset && + coins[selectedAsset].withdrawal_fees && + Object.keys(coins[selectedAsset]?.withdrawal_fees).length && + coins[selectedAsset].withdrawal_fees[getWithdrawNetworkOptions]?.value + ? coins[selectedAsset].withdrawal_fees[getWithdrawNetworkOptions]?.value + : selectedAsset && + coins[selectedAsset].withdrawal_fees && + Object.keys(coins[selectedAsset]?.withdrawal_fees).length && + coins[selectedAsset].withdrawal_fees[ + Object.keys(coins[selectedAsset]?.withdrawal_fees)[0] + ]?.value + ? coins[selectedAsset].withdrawal_fees[ + Object.keys(coins[selectedAsset]?.withdrawal_fees)[0] + ]?.value + : selectedAsset && coins[selectedAsset].withdrawal_fee + ? coins[selectedAsset]?.withdrawal_fee + : 0; +}; + +export const calculateFeeCoin = ( + selectedAsset, + getWithdrawNetworkOptions, + coins +) => { + return selectedAsset && + coins[selectedAsset].withdrawal_fees && + Object.keys(coins[selectedAsset]?.withdrawal_fees).length && + coins[selectedAsset].withdrawal_fees[getWithdrawNetworkOptions]?.symbol + ? coins[selectedAsset].withdrawal_fees[getWithdrawNetworkOptions]?.symbol + : selectedAsset && + coins[selectedAsset].withdrawal_fees && + Object.keys(coins[selectedAsset]?.withdrawal_fees).length && + coins[selectedAsset].withdrawal_fees[ + Object.keys(coins[selectedAsset]?.withdrawal_fees)[0] + ]?.symbol + ? coins[selectedAsset].withdrawal_fees[ + Object.keys(coins[selectedAsset]?.withdrawal_fees)[0] + ]?.symbol + : selectedAsset; +}; + +export const onHandleSymbol = (value) => { + const regex = /\(([^)]+)\)/; + const match = value.match(regex); + const curr = match ? match[1].toLowerCase() : null; + return curr; +}; + +export const renderNetworkWithLabel = (iconId, network) => { + return network && iconId ? ( +
+ {getNetworkNameByKey(network)} +
+ +
+
+ ) : null; +}; diff --git a/web/src/containers/WithdrawConfirmation/index.js b/web/src/containers/WithdrawConfirmation/index.js index faeb8e902b..ac49b9d306 100644 --- a/web/src/containers/WithdrawConfirmation/index.js +++ b/web/src/containers/WithdrawConfirmation/index.js @@ -54,6 +54,7 @@ class ConfirmWithdrawal extends Component { if (!confirm) { const { currency, + fee_coin, amount, address, fee, @@ -106,7 +107,7 @@ class ConfirmWithdrawal extends Component { :{' '} {' '} - {fee} {currency?.toUpperCase()} + {fee} {(fee_coin || currency)?.toUpperCase()} {' '}

diff --git a/web/src/containers/_containers.scss b/web/src/containers/_containers.scss index fc3ac05568..4bf1eba815 100644 --- a/web/src/containers/_containers.scss +++ b/web/src/containers/_containers.scss @@ -28,3 +28,4 @@ @import './Stake/Stake'; @import './Apps/Apps'; @import './CoinPage/CoinPage'; +@import './Summary/components/ReferralList'; diff --git a/web/src/containers/index.js b/web/src/containers/index.js index b4f39a1e68..b3af6a35ff 100644 --- a/web/src/containers/index.js +++ b/web/src/containers/index.js @@ -37,6 +37,8 @@ export { default as DigitalAssets } from './DigitalAssets'; export { default as CoinPage } from './CoinPage'; export { default as WhiteLabel } from './WhiteLabel'; export { default as FeesAndLimits } from './FeesAndLimits'; +export { default as ReferralList } from './Summary/components/ReferralList'; +export { default as P2P } from './P2P'; // ADMIN PAGE export { default as AdminDashboard } from './Admin/Dashboard'; diff --git a/web/src/index.css b/web/src/index.css index 95f225663a..64c9692854 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1727,6 +1727,15 @@ table th { color: #34363a; animation: dotFlashing 3s infinite; } +.layout-mobile .estimated-balance-wrapper .balance-wrapper { + color: var(--labels_important-active-labels-text-graphics); } + .layout-mobile .estimated-balance-wrapper .balance-wrapper .estimated-balance-label { + font-size: 16px; + font-weight: bold; } + .layout-mobile .estimated-balance-wrapper .balance-wrapper .estimated-balance-amount { + font-family: 'Raleway'; + font-size: 4rem; } + .layout-mobile .wallet-container .action_notification-wrapper .action_notification-text { font-size: 1.2rem !important; line-height: 1.2rem !important; } @@ -1742,12 +1751,20 @@ table th { .layout-mobile .wallet-container .wallet-assets_block { padding: 6px; } + .layout-mobile .wallet-container .wallet-assets_block .profit-loss-preformance { + color: var(--labels_important-active-labels-text-graphics); } + .layout-mobile .wallet-container .wallet-assets_block .wallet-assets_block-table thead tr .mobile-balance-header { + text-align: end !important; } .layout-mobile .wallet-container .wallet-assets_block .wallet-assets_block-table thead tr th:nth-child(3) { - text-align: center; } + text-align: end; } .layout-mobile .wallet-container .wallet-assets_block .wallet-assets_block-table .td-wallet, .layout-mobile .wallet-container .wallet-assets_block .wallet-assets_block-table .td-amount, .layout-mobile .wallet-container .wallet-assets_block .wallet-assets_block-table .td-name { min-width: unset !important; } + .layout-mobile .wallet-container .wallet-assets_block .wallet-assets_block-table .td-amount .more-icon-wrapper svg { + width: 3rem; + height: 3rem; + fill: var(--labels_important-active-labels-text-graphics) !important; } .layout-mobile .wallet-container .wallet-assets_block .wallet-assets_block-table .td-wallet .deposit-withdrawal-wrapper { margin: 0 auto; width: 18rem; } @@ -1763,7 +1780,43 @@ table th { .layout-mobile .wallet-container .wallet-assets_block .wallet-assets_block-table tfoot { font-size: 13px !important; } .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper { - flex-direction: column; } + flex-direction: column; + font-weight: bold; + color: var(--labels_important-active-labels-text-graphics); } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .edit-wrapper__container .tooltip_icon_wrapper div { + width: 100%; } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .wallet-search-label { + font-size: 14px; } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .wallet-search-icon { + padding: 1.5%; + border-radius: 20px; + align-items: center; + display: flex; + justify-content: center; + background-color: var(--specials_buttons-links-and-highlights); } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .wallet-search-field .edit-wrapper__container { + width: 100%; } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .wallet-search-field .edit-wrapper__container .field-children div div { + width: 6%; } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .wallet-search-field .edit-wrapper__container .field-children div input { + width: 100%; } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .wallet-search-field .edit-wrapper__container .field-content-outline:before, + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .wallet-search-field .edit-wrapper__container .field-content-outline:after { + height: unset !important; } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .wallet-search-field .edit-wrapper__container .field-children { + border: 1px solid var(--labels_secondary-inactive-label-text-graphics); + border-radius: 5px; } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .wallet-search-field .field-error-content { + display: none; } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .wallet-search-field .edit-wrapper__container div:nth-child(1) { + width: 100%; } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .hide-zero-balance-checkbox .ant-checkbox-inner { + background-color: var(--base_wallet-sidebar-and-popup); } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .hide-zero-balance-checkbox .ant-checkbox-inner:focus, + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .hide-zero-balance-checkbox .ant-checkbox-inner:focus-within { + border-color: var(--labels_important-active-labels-text-graphics) !important; } + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .hide-zero-balance-field { + gap: 10px; } .layout-mobile .wallet-container .action-button-wrapper { padding: 5px 2px; } @@ -1788,6 +1841,8 @@ table th { margin: 5rem 0rem !important; } @media screen and (max-width: 550px) { + .layout-mobile .wallet-container .wallet-assets_block .zero-balance-wrapper .edit-wrapper__container .tooltip_icon_wrapper div { + width: 1.5rem; } .layout-mobile .button-container .holla-button { font-size: 10px !important; } .layout-mobile .currency-wallet-wrapper .wallet-content-wrapper { @@ -1842,7 +1897,13 @@ table th { .dust-dialog-content .large-font { font-size: 4.5rem !important; } -.wallet-container { +.wallet-container .fill_secondary-color { + color: var(--labels_secondary-inactive-label-text-graphics); } + +.wallet-container .fill-active-color { + color: var(--labels_important-active-labels-text-graphics); } + +.wallet-container .header-wrapper { border-top: 1px solid var(--labels_important-active-labels-text-graphics); } .wallet-container .header-wrapper .image-Wrapper .action_notification-svg svg { width: 1.25rem; @@ -1852,128 +1913,145 @@ table th { padding: 2px; } .wallet-container .header-wrapper .action_notification-svg { padding-top: 7px; } - .wallet-container.no-border { - border: none !important; } - .wallet-container .ant-switch { - background-color: var(--base_background); - border: 2px solid var(--labels_secondary-inactive-label-text-graphics); } - .wallet-container .ant-switch .ant-switch-handle { - width: 15px !important; - height: 15px !important; } - .wallet-container .ant-switch .ant-switch-handle:before { - background-color: var(--labels_secondary-inactive-label-text-graphics); } - .wallet-container .ant-switch-checked { - background-color: var(--base_background); - border: 2px solid var(--labels_important-active-labels-text-graphics); } - .wallet-container .ant-switch-checked .ant-switch-handle { - left: calc(100% - 15px - 2px); } - .wallet-container .ant-switch-checked .ant-switch-handle:before { - background-color: var(--labels_important-active-labels-text-graphics); } - .wallet-container .wallet-header_block > * { - margin-bottom: 2rem; } - .wallet-container .wallet-header_block .wallet-header_block-currency_title { - font-size: 1.4rem; - position: relative; } - .wallet-container .wallet-header_block .with_price-block_amount-value { - font-size: 4.5rem; - line-height: 4.5rem; } - .wallet-container .paper-clip-icon .action_notification-svg { - background-color: var(--specials_buttons-links-and-highlights); - width: 1.2rem; - height: 1.2rem; - border-radius: 50%; - display: flex; - margin-left: 5px; } - .wallet-container .paper-clip-icon .action_notification-svg svg { - width: 0.8rem !important; - height: 0.8rem !important; } - .wallet-container .wallet-buttons_action { - margin: 2.5rem 0 3.75rem; } - .wallet-container .wallet-buttons_action > *:first-child { - flex: 2; } - .wallet-container .wallet-buttons_action > *:last-child { - flex: 1; } - .wallet-container .wallet-buttons_action .separator { - width: 1rem; } - .wallet-container .accordion_wrapper .accordion_section_content_text { - color: var(--labels_important-active-labels-text-graphics) !important; } - .wallet-container .accordion_wrapper .accordion_section_content_text:after { - border-left-color: var(--calculated_important-border) !important; } - .wallet-container .empty-wallet-assets_block { - padding: 20px !important; } - .wallet-container .wallet-assets_block { - background-color: var(--base_wallet-sidebar-and-popup); - padding: 0px 20px; - overflow-y: auto; } - .wallet-container .wallet-assets_block .donut-container { - width: 10rem !important; - min-width: 10rem !important; } - .wallet-container .wallet-assets_block .donut-container-empty { - width: 13rem !important; - min-width: 13rem !important; } - .wallet-container .wallet-assets_block .td-wallet { - min-width: 225px; } - .wallet-container .wallet-assets_block .td-wallet .deposit-withdrawal-wrapper { - margin-left: -3rem; - width: 14rem; } - .wallet-container .wallet-assets_block .wallet-assets_block-table { - width: 100%; - font-size: 1rem; - margin-bottom: 3rem; } - .wallet-container .wallet-assets_block .wallet-assets_block-table thead tr, - .wallet-container .wallet-assets_block .wallet-assets_block-table tfoot tr { - height: 3.5rem; - vertical-align: bottom; } - .wallet-container .wallet-assets_block .wallet-assets_block-table tfoot { - font-size: 1.1rem; } - .wallet-container .wallet-assets_block .wallet-assets_block-table tfoot td { - white-space: nowrap; } - .wallet-container .wallet-assets_block .wallet-assets_block-table .table-row { - height: 2.5rem; } - .wallet-container .wallet-assets_block .wallet-assets_block-table .table-bottom-border td, - .wallet-container .wallet-assets_block .wallet-assets_block-table th { - position: relative; } - .wallet-container .wallet-assets_block .wallet-assets_block-table .table-bottom-border td:not(:first-child), - .wallet-container .wallet-assets_block .wallet-assets_block-table th:not(:first-child) { - border-bottom: 1px solid var(--calculated_important-border); - padding: 1rem 0.25rem; } - .wallet-container .wallet-assets_block .wallet-assets_block-table .td-fit { - width: 1%; + .wallet-container .header-wrapper .link { + border-right: 1px solid var(--calculated_secondary-border); + padding-right: 0.5rem; + padding-left: 0.5rem; } + +.wallet-container.no-border { + border: none !important; } + +.wallet-container .ant-switch { + background-color: var(--base_background); + border: 2px solid var(--labels_secondary-inactive-label-text-graphics); } + .wallet-container .ant-switch .ant-switch-handle { + width: 15px !important; + height: 15px !important; } + .wallet-container .ant-switch .ant-switch-handle:before { + background-color: var(--labels_secondary-inactive-label-text-graphics); } + +.wallet-container .ant-switch-checked { + background-color: var(--base_background); + border: 2px solid var(--labels_important-active-labels-text-graphics); } + .wallet-container .ant-switch-checked .ant-switch-handle { + left: calc(100% - 15px - 2px); } + .wallet-container .ant-switch-checked .ant-switch-handle:before { + background-color: var(--labels_important-active-labels-text-graphics); } + +.wallet-container .wallet-header_block > * { + margin-bottom: 2rem; } + +.wallet-container .wallet-header_block .wallet-header_block-currency_title { + font-size: 1.4rem; + position: relative; } + +.wallet-container .wallet-header_block .with_price-block_amount-value { + font-size: 4.5rem; + line-height: 4.5rem; } + +.wallet-container .paper-clip-icon .action_notification-svg { + background-color: var(--specials_buttons-links-and-highlights); + width: 1.2rem; + height: 1.2rem; + border-radius: 50%; + display: flex; + margin-left: 5px; } + .wallet-container .paper-clip-icon .action_notification-svg svg { + width: 0.8rem !important; + height: 0.8rem !important; } + +.wallet-container .wallet-buttons_action { + margin: 2.5rem 0 3.75rem; } + .wallet-container .wallet-buttons_action > *:first-child { + flex: 2; } + .wallet-container .wallet-buttons_action > *:last-child { + flex: 1; } + .wallet-container .wallet-buttons_action .separator { + width: 1rem; } + +.wallet-container .accordion_wrapper .accordion_section_content_text { + color: var(--labels_important-active-labels-text-graphics) !important; } + .wallet-container .accordion_wrapper .accordion_section_content_text:after { + border-left-color: var(--calculated_important-border) !important; } + +.wallet-container .empty-wallet-assets_block { + padding: 20px !important; } + +.wallet-container .wallet-assets_block { + background-color: var(--base_wallet-sidebar-and-popup); + padding: 0px 20px; + overflow-y: auto; } + .wallet-container .wallet-assets_block .donut-container { + width: 10rem !important; + min-width: 10rem !important; } + .wallet-container .wallet-assets_block .donut-container-empty { + width: 13rem !important; + min-width: 13rem !important; } + .wallet-container .wallet-assets_block .td-wallet { + min-width: 225px; } + .wallet-container .wallet-assets_block .td-wallet .deposit-withdrawal-wrapper { + margin-left: -3rem; + width: 14rem; } + .wallet-container .wallet-assets_block .wallet-assets_block-table { + width: 100%; + font-size: 1rem; + margin-bottom: 3rem; } + .wallet-container .wallet-assets_block .wallet-assets_block-table thead tr, + .wallet-container .wallet-assets_block .wallet-assets_block-table tfoot tr { + height: 3.5rem; + vertical-align: bottom; } + .wallet-container .wallet-assets_block .wallet-assets_block-table tfoot { + font-size: 1.1rem; } + .wallet-container .wallet-assets_block .wallet-assets_block-table tfoot td { white-space: nowrap; } - .wallet-container .wallet-assets_block .wallet-assets_block-table .td-name { - min-width: 200px !important; } - .wallet-container .wallet-assets_block .wallet-assets_block-table .table-icon > div { - vertical-align: middle; - margin: auto; } - .wallet-container .wallet-assets_block .wallet-assets_block-table .td-amount { - min-width: 150px !important; - direction: ltr; } - .wallet-container .wallet-assets_block .search-field { - background-color: var(--base_wallet-sidebar-and-popup); } - .wallet-container .wallet-assets_block .loading-anime { - width: 100px; - height: 20px; - border-radius: 7px; - animation: mymove 3s infinite; } - .wallet-container .wallet-assets_block .loading-row-anime { - height: 20px; - border-radius: 7px; - animation: mymove 3s infinite; } - .wallet-container .wallet-assets_block .loading-row-anime.w-half { - width: 100px; } - .wallet-container .wallet-assets_block .loading-row-anime.w-full { - width: 200px; } - .wallet-container .wallet-assets_block .wallet-graphic-icon { - opacity: 0.1; } - .wallet-container .accordion_wrapper .action_notification-wrapper { - position: relative; } - .wallet-container .action-button-wrapper { - border: 1px solid var(--specials_buttons-links-and-highlights); - padding: 5px 12px; } - .wallet-container .action-button-wrapper:hover { - background-color: var(--specials_buttons-links-and-highlights); } - .wallet-container .action-button-wrapper:hover .notification-info { - color: white !important; } + .wallet-container .wallet-assets_block .wallet-assets_block-table .table-row { + height: 2.5rem; } + .wallet-container .wallet-assets_block .wallet-assets_block-table .table-bottom-border td, + .wallet-container .wallet-assets_block .wallet-assets_block-table th { + position: relative; } + .wallet-container .wallet-assets_block .wallet-assets_block-table .table-bottom-border td:not(:first-child), + .wallet-container .wallet-assets_block .wallet-assets_block-table th:not(:first-child) { + border-bottom: 1px solid var(--calculated_important-border); + padding: 1rem 0.25rem; } + .wallet-container .wallet-assets_block .wallet-assets_block-table .td-fit { + width: 1%; + white-space: nowrap; } + .wallet-container .wallet-assets_block .wallet-assets_block-table .td-name { + min-width: 200px !important; } + .wallet-container .wallet-assets_block .wallet-assets_block-table .table-icon > div { + vertical-align: middle; + margin: auto; } + .wallet-container .wallet-assets_block .wallet-assets_block-table .td-amount { + min-width: 150px !important; + direction: ltr; } + .wallet-container .wallet-assets_block .search-field { + background-color: var(--base_wallet-sidebar-and-popup); } + .wallet-container .wallet-assets_block .loading-anime { + width: 100px; + height: 20px; + border-radius: 7px; + animation: mymove 3s infinite; } + .wallet-container .wallet-assets_block .loading-row-anime { + height: 20px; + border-radius: 7px; + animation: mymove 3s infinite; } + .wallet-container .wallet-assets_block .loading-row-anime.w-half { + width: 100px; } + .wallet-container .wallet-assets_block .loading-row-anime.w-full { + width: 200px; } + .wallet-container .wallet-assets_block .wallet-graphic-icon { + opacity: 0.1; } + +.wallet-container .accordion_wrapper .action_notification-wrapper { + position: relative; } + +.wallet-container .action-button-wrapper { + border: 1px solid var(--specials_buttons-links-and-highlights); + padding: 5px 12px; } + .wallet-container .action-button-wrapper:hover { + background-color: var(--specials_buttons-links-and-highlights); } + .wallet-container .action-button-wrapper:hover .notification-info { + color: white !important; } .show-equals:after { content: '='; @@ -2132,6 +2210,26 @@ table th { display: flex; justify-content: center; } +.layout-mobile .footer-button { + margin-top: 4%; + margin-bottom: 2%; } + +.layout-mobile .bottom-bar-button { + padding: 3%; + display: flex; + justify-content: space-around; } + .layout-mobile .bottom-bar-button .bottom-bar-deposit-button, + .layout-mobile .bottom-bar-button .bottom-bar-withdraw-button { + width: 45%; } + .layout-mobile .bottom-bar-button .bottom-bar-deposit-button button, + .layout-mobile .bottom-bar-button .bottom-bar-withdraw-button button { + border-radius: 5px; } + .layout-mobile .bottom-bar-button .bottom-bar-withdraw-button button { + color: var(--labels_important-active-labels-text-graphics); + background-color: var(--base_wallet-sidebar-and-popup) !important; + border-radius: 5px; + border-color: var(--labels_important-active-labels-text-graphics) !important; } + .review-wrapper .review-icon { width: 8rem; height: 8rem; @@ -2190,25 +2288,259 @@ table th { .bank_account-information-wrapper { margin: 2rem 0; } -.withdrawal-container .icon_title-wrapper { - flex-direction: row; - justify-content: flex-start; } +.withdrawal-container { + width: 75rem !important; } + .withdrawal-container .transaction-history-wrapper { + max-width: 100% !important; } + .withdrawal-container .icon_title-wrapper { + flex-direction: row; + justify-content: flex-start; } + .withdrawal-container .icon_title-wrapper .icon_title-text { + font-size: 2.5rem; } + .withdrawal-container .withdraw-icon .icon_title-svg { + width: unset !important; + height: unset !important; } + .withdrawal-container .withdraw-icon .icon_title-svg svg { + width: 3rem !important; + height: 3rem !important; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form { + border-radius: 4px; + margin: 0 0 40px; + background-color: var(--base_wallet-sidebar-and-popup); + padding: 30px 30px 30px 60px; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .field-children .field-valid { + top: 8px; + right: -20px; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-select-input-style { + width: 80%; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-deposit-icon-wrapper div { + justify-content: unset; + align-items: unset; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-deposit-disable { + opacity: 0.5; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-deposit-icon { + width: 100%; + height: 2.5rem; + border: 2px solid var(--labels_important-active-labels-text-graphics); + border-radius: 100%; + padding: 2px; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-deposit-content { + font-size: 14px; + padding: 1% 1%; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .destination-input-field { + width: 80%; + height: 38px; + padding: 10px 10px; + border-radius: 2px; + border: 1px solid var(--calculated_important-border); + background-color: var(--base_wallet-sidebar-and-popup); + color: var(--labels_important-active-labels-text-graphics); } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .destination-input-field input { + background-color: var(--base_wallet-sidebar-and-popup); + color: var(--labels_important-active-labels-text-graphics); + text-overflow: ellipsis; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .destination-input-field:hover { + border: 1px solid var(--labels_important-active-labels-text-graphics); } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .field-error { + border: 2px solid var(--specials_notifications-alerts-warnings); } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .field-error:hover { + border: 2px solid var(--specials_notifications-alerts-warnings); } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-main-label { + color: var(--labels_secondary-inactive-label-text-graphics); + font-size: 14px; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-main-label-selected { + color: var(--labels_important-active-labels-text-graphics); + text-wrap: nowrap; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .side-icon-wrapper { + display: flex; + width: 15% !important; + height: 25%; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .side-icon-wrapper svg { + width: 100%; + opacity: 0.1; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-step, + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-step-selected { + border: 2px solid var(--labels_secondary-inactive-label-text-graphics); + border-radius: 100%; + width: fit-content; + padding: 3px 10px; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-step-selected { + border: 2px solid var(--labels_important-active-labels-text-graphics); } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line, + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line-selected, + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line-selected-large { + border: 1px solid var(--labels_secondary-inactive-label-text-graphics); + height: 60px; + margin: 0 100%; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line-selected { + height: 70px; + border: 1px solid var(--labels_important-active-labels-text-graphics); } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line-selected-large { + height: 70px; + border: 1px solid var(--labels_important-active-labels-text-graphics); } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line-extra-large { + height: 2rem; + border: 1px solid var(--labels_secondary-inactive-label-text-graphics); + margin: 0 100%; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line-extra-large-active { + border: 1px solid var(--labels_important-active-labels-text-graphics) !important; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-label { + text-wrap: nowrap; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .amount-field-icon { + margin-top: 1%; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-wrapper { + display: flex; + flex-direction: column; + width: 65%; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-wrapper .select-field:hover { + border: 1px solid var(--labels_important-active-labels-text-graphics); + border-radius: 0.35rem; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-wrapper .disabled .ant-select-show-search.ant-select-single:not(.ant-select-customize-input) .ant-select-selector { + cursor: not-allowed !important; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-wrapper .ant-select-show-search.ant-select-single:not(.ant-select-customize-input) .ant-select-selector { + cursor: pointer !important; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-wrapper .custom-select-input-style .ant-select-selection-search-input { + height: 100% !important; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-wrapper .ant-select-single .ant-select-selector .ant-select-selection-placeholder { + margin-top: 1%; + pointer-events: unset !important; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-wrapper .custom-select-input-style .ant-select-selection-item { + line-height: 40px !important; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-wrapper .custom-select-input-style .ant-select-selector { + height: 40px !important; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-deposit-disable .select-field:hover { + border: var(--labels_secondary-inactive-label-text-graphics); } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .opacity-100 { + opacity: 1; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .opacity-50 { + opacity: 0.5; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .opacity-30 { + opacity: 0.3; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .ant-select-clear { + user-select: none; + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + background-color: var(--labels_important-active-labels-text-graphics); } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .ant-select-clear svg { + color: var(--base_wallet-sidebar-and-popup); + font-size: 18px; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .ant-input-suffix { + width: 5rem; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .render-amount-icon-wrapper { + color: var(--specials_buttons-links-and-highlights); + cursor: pointer; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .render-amount-icon-wrapper .suffix-text { + padding-right: 0.5rem; + text-transform: uppercase; + font-size: 15px; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .render-amount-icon-wrapper .suffix-text .edit-wrapper__container { + text-decoration: underline; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .render-amount-icon-wrapper .img-wrapper img { + height: 1.5rem; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .currency-label { + border: 1px solid var(--labels_secondary-inactive-label-text-graphics); + padding: 0.5%; + border-radius: 5px; + margin-right: 10px; + width: 5rem; + text-align: center; + cursor: pointer; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .currency-label .pinned-asset-icon { + padding-top: 3%; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .render-scan-wrapper { + color: var(--specials_buttons-links-and-highlights); + cursor: pointer; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .render-scan-wrapper .suffix-text { + padding-right: 0.5rem; + font-size: 15px; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .render-scan-wrapper .suffix-text .edit-wrapper__container { + text-decoration: underline; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .render-scan-wrapper .img-wrapper { + display: flex; + align-items: center; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .render-scan-wrapper .img-wrapper img { + height: 1rem; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .destination-field { + height: 60px; } + .withdrawal-container .withdraw-form-wrapper .withdraw-form .destination-field-mobile { + height: 6rem; } + .withdrawal-container .withdraw-form-wrapper .bottom-content { + margin-top: 3%; } + .withdrawal-container .withdraw-form-wrapper .bottom-content .fee-fields { + color: var(--labels_secondary-inactive-label-text-graphics); } + .withdrawal-container .withdraw-form-wrapper .btn-wrapper { + display: flex; + justify-content: center; } + .withdrawal-container .withdraw-form-wrapper .btn-wrapper .holla-button { + width: 300px; } + .withdrawal-container .withdraw-form-wrapper .withdraw-btn-wrapper { + display: flex; + margin-top: 10%; + width: 80%; } + .withdrawal-container .withdraw-form-wrapper .withdraw-btn-wrapper .ant-btn[disabled], + .withdrawal-container .withdraw-form-wrapper .withdraw-btn-wrapper .ant-btn[disabled]:hover, + .withdrawal-container .withdraw-form-wrapper .withdraw-btn-wrapper .ant-btn[disabled]:focus, + .withdrawal-container .withdraw-form-wrapper .withdraw-btn-wrapper .ant-btn[disabled]:active, + .withdrawal-container .withdraw-form-wrapper .withdraw-btn-wrapper .ant-btn { + background: var(--labels_inactive-button); + border: unset; + width: 300px; + font-family: 'Raleway'; + color: var(--base_wallet-sidebar-and-popup); + font-size: 1.1rem; } + .withdrawal-container .withdraw-form-wrapper .withdraw-btn-wrapper .ant-btn { + padding: 0 4px; + background: var(--specials_buttons-links-and-highlights); + color: var(--labels_important-active-labels-text-graphics); } + .withdrawal-container .withdraw-form-wrapper .withdraw-btn-wrapper .holla-button { + width: 300px; } + .withdrawal-container .withdraw-form-wrapper .warning-text, + .withdrawal-container .withdraw-form-wrapper .email-text { + color: var(--specials_pending-waiting-caution); + font-size: 12px; + width: 90%; } + .withdrawal-container .withdraw-form-wrapper .email-text { + color: var(--labels_secondary-inactive-label-text-graphics) !important; } + .withdrawal-container .withdraw-form-wrapper .width-80 { + width: 80%; } + .withdrawal-container .withdraw-form-wrapper .check-optional .ant-checkbox-inner { + background-color: var(--base_wallet-sidebar-and-popup) !important; } + .withdrawal-container .withdraw-form-wrapper .check-optional .ant-checkbox-checked .ant-checkbox-inner { + border-color: var(--labels_important-active-labels-text-graphics) !important; } -.withdrawal-container .withdraw-form-wrapper .withdraw-form { - border-radius: 4px; - margin: 0 0 40px; - background-color: var(--base_wallet-sidebar-and-popup); - padding: 30px; } - .withdrawal-container .withdraw-form-wrapper .withdraw-form .field-children .field-valid { - top: 8px; - right: -20px; } +.withdrawal-remove-tag-modal .ant-modal-header { + border: none; } -.withdrawal-container .withdraw-form-wrapper .btn-wrapper { - display: flex; - justify-content: center; - align-items: center; } - .withdrawal-container .withdraw-form-wrapper .btn-wrapper .holla-button { - width: 300px; } +.withdrawal-remove-tag-modal .ant-modal-header, +.withdrawal-remove-tag-modal .ant-modal-body { + background-color: var(--base_wallet-sidebar-and-popup) !important; } + +.withdrawal-remove-tag-modal .ant-modal-body { + padding-top: 0 !important; } + +.withdrawal-remove-tag-modal .remove-tag-wrapper, +.withdrawal-remove-tag-modal .warning-popup-wrapper { + color: var(--labels_important-active-labels-text-graphics); } + .withdrawal-remove-tag-modal .remove-tag-wrapper .tag-body, + .withdrawal-remove-tag-modal .warning-popup-wrapper .tag-body { + height: 18rem; } + .withdrawal-remove-tag-modal .remove-tag-wrapper .button-wrapper, + .withdrawal-remove-tag-modal .warning-popup-wrapper .button-wrapper { + display: flex; + justify-content: space-between; } + .withdrawal-remove-tag-modal .remove-tag-wrapper .button-wrapper Button, + .withdrawal-remove-tag-modal .warning-popup-wrapper .button-wrapper Button { + width: 48%; + height: 3rem; + border: none !important; + color: var(--labels_important-active-labels-text-graphics) !important; + font-size: 14px; } + +.withdrawal-remove-tag-modal .warning-popup-wrapper .button-wrapper { + margin-top: 5%; } + .withdrawal-remove-tag-modal .warning-popup-wrapper .button-wrapper Button { + width: 100%; } .layout-mobile .review-wrapper { min-width: 0; @@ -2258,25 +2590,97 @@ table th { .layout-mobile .presentation_container .inner_container .holla-button { font-size: 13px !important; } +.layout-mobile .withdrawal-remove-tag-modal .remove-tag-wrapper .button-wrapper Button { + height: 4rem; + font-size: 12px !important; + margin-top: 3%; } + .layout-mobile .review_email-wrapper { margin: 0px auto !important; } -.layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .field-wrapper .field-label-wrapper { - flex-direction: column; } +.layout-mobile .withdrawal-container { + padding-top: 0px !important; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form { + margin: 0 0 30px; + padding: 5%; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .destination-field { + height: 75px; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .destination-field-mobile { + height: 11rem; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .field-wrapper .field-label-wrapper { + flex-direction: column; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-main-label-selected { + font-size: 14px; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-deposit-content { + padding: 0 1%; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-line-selected-large { + height: 13rem; + border: 1px solid var(--labels_important-active-labels-text-graphics); } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line-selected { + height: 100%; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line { + height: 13rem; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line-network-selected { + height: 15rem; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-line-selected-mobile { + height: 9rem; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .mobile-view { + margin-top: 5%; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .ant-select-clear svg { + font-size: 18px; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-wrapper { + width: 100%; + margin-left: 5%; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .bottom-content { + font-size: 14px; + margin-top: 3%; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-main-label-selected, + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-main-label, + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .warning-text { + font-size: 14px; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .currency-label { + width: 15%; + font-size: 14px; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .render-scan-wrapper .suffix-text { + font-size: 12px; } @media screen and (max-width: 550px) { + .layout-mobile .presentation_container .inner_container .withdraw-icon { + margin-top: 0px !important; } .layout-mobile .presentation_container .inner_container .information_block .information_block-text_wrapper .title { font-size: 14px !important; } .layout-mobile .presentation_container .inner_container .information_block .information_block-text_wrapper .text { - font-size: 12px !important; } } + font-size: 12px !important; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form { + margin: 0 0 20px; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .select-wrapper { + margin-left: 7%; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .ant-select-clear svg { + font-size: 12px; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-step-selected, + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-field .custom-step { + padding: 33% 70% 10%; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-select-input-style .ant-select-selector { + font-size: 1.5rem !important; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-select-input-style .ant-select-selection-item { + margin-top: 4%; + line-height: unset !important; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .currency-label { + font-size: 12px; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .mobile-view { + margin-top: 3%; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .amount-field-icon { + margin-top: 2%; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .warning-text { + font-size: 10px; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-line-selected-large { + height: 17rem; } } @media screen and (max-width: 390px) { - .layout-mobile .presentation_container .inner_container .information_block .information_block-text_wrapper .title { - font-size: 13px !important; } - .layout-mobile .presentation_container .inner_container .information_block .information_block-text_wrapper .text { - font-size: 10px !important; } - .layout-mobile .presentation_container .inner_container .holla-button { - font-size: 12px !important; } } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .custom-line-selected-large { + height: 18rem; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .withdraw-btn-wrapper { + margin-top: 20%; } } .review_email-wrapper { margin: auto; } @@ -2284,14 +2688,6 @@ table th { border-top: 1px solid; padding: 1rem; } -@media screen and (max-width: 1096px) { - .withdrawal-container .withdraw-form-wrapper .withdraw-form .field-wrapper .field-label { - font-size: 13px; } - .withdrawal-container .withdraw-form-wrapper .withdraw-form .field-wrapper .input-box-field .input_field-input::placeholder { - font-size: 13px; } - .withdrawal-container .withdraw-form-wrapper .btn-wrapper .holla-button { - width: 200px; } } - .qr_reader_wrapper { width: 25rem; margin: 2rem; } @@ -2325,6 +2721,13 @@ table th { width: 11rem !important; height: 11rem !important; } +.qr-popup-buttons { + display: flex; + justify-content: space-between; } + .qr-popup-buttons .qr-back-btn, + .qr-popup-buttons .qr-copy-btn { + width: 45%; } + .deposit_info-qr-wrapper .qr-text { text-align: center; display: flex; @@ -2361,6 +2764,130 @@ table th { .multiple-actions-wrapper .mini-qr { fill: var(--specials_buttons-links-and-highlights); } +.deposit-wrapper-fields .currency-label { + border: 1px solid var(--labels_secondary-inactive-label-text-graphics); + padding: 0.5%; + border-radius: 5px; + margin-right: 10px; + width: 4rem; + text-align: center; + cursor: pointer; } + +.deposit-wrapper-fields .step3-icon-wrapper { + height: 0%; + width: fit-content; } + +.deposit-wrapper-fields .btn-wrapper-deposit { + margin-left: 3%; } + .deposit-wrapper-fields .btn-wrapper-deposit .holla-button { + background-color: var(--specials_buttons-links-and-highlights); + color: var(--labels_important-active-labels-text-graphics); + width: 300px; + border: none; } + +.deposit-wrapper-fields .generate-field-wrapper { + display: flex; + flex-direction: column; + width: 55%; + justify-content: space-around; + margin-right: 15%; } + +.deposit-wrapper-fields .deposit-network-field .ant-select-selection-item { + cursor: not-allowed; } + +.deposit-address-wrapper { + width: 68.5%; } + .deposit-address-wrapper .deposit-address-field .destination-input-field, + .deposit-address-wrapper .deposit-address-field .destination-input-field:focus-within { + border: none !important; + border-bottom: 1px solid var(--calculated_important-border) !important; } + .deposit-address-wrapper .deposit-address-field .ant-input-affix-wrapper-focused { + box-shadow: none; } + .deposit-address-wrapper .deposit-address-field .ant-input-suffix { + width: 15% !important; } + .deposit-address-wrapper .deposit-address-field .divider { + border-right: 1px solid var(--calculated_secondary-border); + padding-left: 15%; + height: 16px; + margin-top: 4%; } + .deposit-address-wrapper .deposit-address-field .render-deposit-scan-wrapper { + text-transform: uppercase; + color: var(--specials_buttons-links-and-highlights); + cursor: pointer; } + .deposit-address-wrapper .deposit-address-field .render-deposit-scan-wrapper div { + text-decoration: underline; } + .deposit-address-wrapper .deposit-address-field .ant-input { + color: var(--specials_buttons-links-and-highlights) !important; } + .deposit-address-wrapper .address-warning-text div { + text-wrap: wrap; + width: 95%; + justify-content: unset !important; } + +.destination-tag-field-wrapper { + margin-left: 28%; } + .destination-tag-field-wrapper .destination-tag-field .destination-input-field, + .destination-tag-field-wrapper .destination-tag-field .destination-input-field:focus-within { + border: none !important; + border-bottom: 1px solid var(--calculated_important-border) !important; } + .destination-tag-field-wrapper .destination-tag-field .destination-input-field { + width: 100% !important; } + .destination-tag-field-wrapper .destination-tag-field .render-deposit-scan-wrapper { + text-transform: uppercase; + color: var(--specials_buttons-links-and-highlights); + cursor: pointer; } + .destination-tag-field-wrapper .destination-tag-field .render-deposit-scan-wrapper div { + text-decoration: underline; } + .destination-tag-field-wrapper .destination-tag-field .ant-input-suffix { + width: auto !important; } + .destination-tag-field-wrapper .destination-tag-field .ant-input:focus, + .destination-tag-field-wrapper .destination-tag-field .ant-input-affix-wrapper-focused { + box-shadow: none; } + .destination-tag-field-wrapper .destination-tag-field .ant-input { + color: var(--specials_buttons-links-and-highlights) !important; } + .destination-tag-field-wrapper .tag-text div { + text-wrap: balance; } + +.withdrawal-container .withdraw-form-wrapper .withdraw-form .deposit-wrapper-fields .custom-field .custom-line-large { + height: 90px; + border: 1px solid var(--labels_important-active-labels-text-graphics); + margin: 0 100%; } + +.withdrawal-container .withdraw-form-wrapper .withdraw-form .deposit-wrapper-fields .custom-field .custom-line-extra-large { + height: 2rem; + border: 1px solid var(--labels_important-active-labels-text-graphics); + margin: 0 100%; } + +.generate-deposit-label { + color: var(--labels_secondary-inactive-label-text-graphics); + margin: 1% 3%; } + +.layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .deposit-wrapper-fields .custom-field .custom-line-large { + height: 11rem; + border: 1px solid var(--labels_important-active-labels-text-graphics); + margin: 0 100%; } + +.layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .deposit-wrapper-fields .generate-field-wrapper { + margin-left: unset; } + +.layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .deposit-wrapper-fields .deposit-address-wrapper { + width: unset; } + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .deposit-wrapper-fields .deposit-address-wrapper .destination-input-field { + width: 88% !important; } + +.layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .destination-tag-field-wrapper .destination-tag-field { + width: unset !important; } + +.layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .destination-tag-field-wrapper .destination-input-field { + width: 80% !important; } + +@media screen and (max-width: 550px) { + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .deposit-wrapper-fields .custom-field .custom-line-large { + height: 17rem; } } + +@media screen and (max-width: 450px) { + .layout-mobile .withdrawal-container .withdraw-form-wrapper .withdraw-form .deposit-wrapper-fields .btn-wrapper-deposit .holla-button { + width: -webkit-fill-available; } } + .transaction-history-wrapper .ant-row, .login-history-section-wrapper .ant-row { margin-left: 0 !important; @@ -4249,6 +4776,8 @@ table th { .summary-container .trade-account-link { color: var(--specials_buttons-links-and-highlights); font-weight: bold; } + .summary-container .deposit-icon svg { + width: 1rem; } .summary-container .summary-section_1 .assets-wrapper .rounded-loading { width: 80px; height: 80px; @@ -4856,6 +5385,15 @@ table th { .digital-market-wrapper .market-list__block-table .table-row .sticky-col:hover { opacity: 0.5; } +.digital-market-wrapper .custom-header-wrapper { + color: var(--labels_secondary-inactive-label-text-graphics); + font-family: "Raleway"; + font-weight: bold; } + +.digital-market-wrapper .custom-border-bottom { + border-bottom: 1px solid var(--labels_important-active-labels-text-graphics); + margin-bottom: 10%; } + .trade_tabs-container .market-pairs { color: var(--labels_important-active-labels-text-graphics); } @@ -6312,6 +6850,403 @@ table th { .hollaex-token-wrapper .token-wrapper .hollaex-container .trade-details-wrapper .trade-details-content .trade_tabs-container { font-size: 1.5rem !important; } } +.referralLabel { + color: var(--labels_important-active-labels-text-graphics); } + +.highChartColor .highcharts-graph { + stroke: var(--labels_important-active-labels-text-graphics); } + +.highChartColor .highcharts-point { + stroke: var(--labels_important-active-labels-text-graphics); + fill: var(--labels_important-active-labels-text-graphics); } + +.referral-list-wrapper { + width: 80%; + margin-left: auto; + margin-right: auto; } + +.fs-13 { + font-size: 13px; } + +.fs-14 { + font-size: 14px; } + +.summary-block_wrapper { + background-color: var(--base_wallet-sidebar-and-popup); } + .summary-block_wrapper .referral-table-wrapper .history-table-link { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 3%; + color: var(--specials_buttons-links-and-highlights); } + .summary-block_wrapper .referral-table-wrapper .referral-copy-link { + color: var(--labels_important-active-labels-text-graphics); + padding: 5px; + cursor: pointer; + background-color: var(--specials_buttons-links-and-highlights); + border-radius: 10px; } + .summary-block_wrapper .referral-table-wrapper .summary-table-link { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 3%; } + +.referral-active-text { + color: var(--labels_important-active-labels-text-graphics); } + +.referral-inActive-text { + color: var(--labels_secondary-inactive-label-text-graphics); } + +.referral-header-icon-wrapper .icon_title-wrapper { + display: flex; + justify-content: flex-start; + flex-direction: row; } + .referral-header-icon-wrapper .icon_title-wrapper .icon_title-text.title { + font-size: 3rem !important; + padding-top: 2.25rem !important; } + +.referral_table_theme .ant-modal-body { + background-color: var(--base_wallet-sidebar-and-popup) !important; } + +.referral_table_theme .ReactModal__Content { + width: 40rem; } + +.back-label { + cursor: pointer; + text-decoration: underline; + color: var(--specials_buttons-links-and-highlights); } + +.summary-referral-container { + padding: 2%; } + .summary-referral-container .summary-referral-wrapper { + display: flex; + justify-content: space-between; } + .summary-referral-container .summary-referral-wrapper .settle-link, + .summary-referral-container .summary-referral-wrapper .view-history-label { + color: var(--specials_buttons-links-and-highlights); + text-decoration: underline; + cursor: pointer; + font-weight: bold; } + .summary-referral-container .summary-referral-wrapper .earning-icon-wrapper { + display: flex; } + .summary-referral-container .summary-referral-wrapper .earning-icon-wrapper .refer-icon { + fill: var(--labels_important-active-labels-text-graphics); } + .summary-referral-container .summary-referral-wrapper .earning-icon-wrapper .margin-aligner { + margin-bottom: 1% !important; } + .summary-referral-container .summary-referral-wrapper .earning-icon-wrapper .earning-label { + font-size: 18px; + font-weight: bold; } + .summary-referral-container .summary-referral-wrapper .earn-info-wrapper { + margin-right: 3%; } + .summary-referral-container .summary-referral-wrapper .earn-info-wrapper .earn-info-border { + border-bottom: 1px solid var(--labels_important-active-labels-text-graphics); } + .summary-referral-container .summary-referral-wrapper .earn-info-wrapper .earn-text-wrapper { + display: flex; + justify-content: center; } + .summary-referral-container .summary-referral-wrapper .earn-info-wrapper .earn-text-wrapper .field-label { + margin-left: 2%; } + .summary-referral-container .summary-referral-wrapper .earn-info-wrapper .settle-btn { + background-color: var(--specials_buttons-links-and-highlights); + color: var(--labels_important-active-labels-text-graphics); + border: none; } + +.history-referral-container { + padding: 2%; } + .history-referral-container .summary-history-referral { + display: flex; + justify-content: space-between !important; } + .history-referral-container .history-referral-wrapper .earning-icon-wrapper { + display: flex; } + .history-referral-container .history-referral-wrapper .earning-icon-wrapper .refer-icon { + fill: var(--labels_important-active-labels-text-graphics); } + .history-referral-container .history-referral-wrapper .earning-icon-wrapper .margin-aligner { + margin-bottom: 1% !important; } + .history-referral-container .history-referral-wrapper .earning-icon-wrapper .earning-label { + font-size: 18px; + font-weight: bold; } + .history-referral-container .history-refer-icon .margin-aligner { + opacity: 0.5; + margin-bottom: unset; + margin-right: unset; } + .history-referral-container .history-refer-icon .margin-aligner svg { + width: 40% !important; + height: 70% !important; } + .history-referral-container .history-refer-icon .margin-aligner div { + display: flex; + justify-content: flex-end; } + +.new-refer-wrapper { + background-color: var(--calculated_quick_trade-bg) !important; + color: var(--labels_important-active-labels-text-graphics); + padding: 15px; } + .new-refer-wrapper .new-refer-icon-wrapper { + display: flex; } + .new-refer-wrapper .new-refer-icon-wrapper .margin-aligner { + margin-bottom: 1% !important; } + .new-refer-wrapper .create-link-label { + color: var(--specials_buttons-links-and-highlights); + text-decoration: underline; + font-weight: bold; + margin-top: 0.5%; + cursor: pointer; } + .new-refer-wrapper .content-field { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 500px; } + .new-refer-wrapper .no-link-icon .margin-aligner { + margin-bottom: 5%; + margin-right: unset; } + .new-refer-wrapper .no-link-icon .margin-aligner svg { + width: 100% !important; + height: 100% !important; } + +.settle-popup-wrapper { + color: var(--labels_important-active-labels-text-graphics); } + .settle-popup-wrapper .earning-icon-wrapper { + display: flex; + align-items: center; } + .settle-popup-wrapper .earning-icon-wrapper .insufficient-icon { + text-align: center; } + .settle-popup-wrapper .earning-icon-wrapper .insufficient-icon svg { + width: 5rem !important; + height: 5rem !important; } + .settle-popup-wrapper .earning-icon-wrapper .margin-aligner svg { + width: 4rem !important; + height: 4rem !important; } + .settle-popup-wrapper .earning-icon-wrapper .refer-icon { + fill: var(--labels_important-active-labels-text-graphics); } + .settle-popup-wrapper .earning-icon-wrapper .margin-aligner { + margin-bottom: 1% !important; } + .settle-popup-wrapper .earning-icon-wrapper .earning-label { + font-size: 18px; + font-weight: bold; } + +.refer-code-popup-wrapper, +.referral-popup-earning-wrapper, +.referral-final-popup { + color: var(--labels_important-active-labels-text-graphics); } + .refer-code-popup-wrapper .new-referral-wrapper .new-referral-label, + .referral-popup-earning-wrapper .new-referral-wrapper .new-referral-label, + .referral-final-popup .new-referral-wrapper .new-referral-label { + font-size: 16px; + margin: 2% 0 3% 2%; } + .refer-code-popup-wrapper .custom-input-field, + .referral-popup-earning-wrapper .custom-input-field, + .referral-final-popup .custom-input-field { + padding: 2%; + width: 100%; + border: 1px solid var(--specials_buttons-links-and-highlights); } + +.referral-popup-earning-wrapper .discount-label { + margin: 5% 0; + font-weight: bold; } + +.referral-popup-earning-wrapper .discount-tooltip { + padding-top: 4%; } + +.referral-popup-earning-wrapper .discount-tooltip-mobile { + padding-top: 3%; } + +.referral-popup-earning-wrapper .earn-field-wrapper { + width: 100%; + border: 1px solid var(--labels_important-active-labels-text-graphics); + border-radius: 10px; + display: flex; + flex-direction: row; + justify-content: space-between; } + .referral-popup-earning-wrapper .earn-field-wrapper .earn-info-border { + border-bottom: 1px solid var(--labels_important-active-labels-text-graphics); } + .referral-popup-earning-wrapper .earn-field-wrapper .eraning-rate-field, + .referral-popup-earning-wrapper .earn-field-wrapper .discount-field, + .referral-popup-earning-wrapper .earn-field-wrapper .eraning-rate-mobile-field, + .referral-popup-earning-wrapper .earn-field-wrapper .discount-mobile-field { + font-size: 12px; + cursor: pointer; + padding: 2%; } + .referral-popup-earning-wrapper .earn-field-wrapper .eraning-rate-mobile-field, + .referral-popup-earning-wrapper .earn-field-wrapper .discount-mobile-field { + padding: 5% !important; } + +.referral-popup-earning-wrapper .earn-border-left { + border-left: 1px solid var(--labels_important-active-labels-text-graphics); + padding-top: 10% !important; + padding-left: 1% !important; } + .referral-popup-earning-wrapper .earn-border-left .caret-icon-mobile svg { + width: 5rem !important; + height: 5rem !important; } + .referral-popup-earning-wrapper .earn-border-left .caret-down-icon-mobile svg { + height: 8rem !important; } + +.referral-popup-earning-wrapper .caret-icon-wrapper { + width: 15%; + text-align: center; + padding-left: 3%; + border-bottom-right-radius: 10px; + border-top-right-radius: 10px; + cursor: pointer; + display: flex; + flex-direction: column; + background-color: var(--base_background); } + .referral-popup-earning-wrapper .caret-icon-wrapper .caret-up-icon, + .referral-popup-earning-wrapper .caret-icon-wrapper .caret-up-icon-mobile { + width: 0; + height: 40%; + line-height: 0; } + .referral-popup-earning-wrapper .caret-icon-wrapper .caret-up-icon-mobile { + height: 15% !important; } + .referral-popup-earning-wrapper .caret-icon-wrapper .caret-down-icon { + width: 0; + height: 0; + line-height: 0; } + .referral-popup-earning-wrapper .caret-icon-wrapper .caret-icon svg { + width: 3rem !important; + height: 3rem !important; } + +.confirm-fild-wrapper { + color: var(--labels_important-active-labels-text-graphics); } + .confirm-fild-wrapper .confirm-field-header { + font-weight: bold; + color: var(--labels_important-active-labels-text-graphics); + font-size: 18px; + margin-bottom: 1%; } + .confirm-fild-wrapper .discount-field { + margin: 2% 0 1% 0; + font-weight: bold; } + .confirm-fild-wrapper .referral-field-content-wrapper { + padding: 2%; + width: 100%; + border: 1px solid var(--labels_important-active-labels-text-graphics); } + .confirm-fild-wrapper .referral-field-content-wrapper .referral-field-content { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 10%; } + .confirm-fild-wrapper .referral-field-content-wrapper .earn-rate-content, + .confirm-fild-wrapper .referral-field-content-wrapper .discount-content { + display: flex; + flex-direction: row; + justify-content: space-between; } + +.referral-final-popup .checked-icon { + padding-top: 2%; } + .referral-final-popup .checked-icon svg { + width: 2rem; + height: 2rem; } + +.referral-final-popup .desc-label { + margin-bottom: 2%; } + +.referral-final-popup .custom-input { + padding: 2%; + width: 100%; + border: 1px solid var(--labels_important-active-labels-text-graphics); + color: var(--specials_buttons-links-and-highlights); + display: flex; + justify-content: space-between; + align-items: center; } + .referral-final-popup .custom-input .link-content { + text-decoration: underline; + cursor: pointer; } + +.referral-popup-btn-wrapper { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-top: 5%; } + .referral-popup-btn-wrapper .back-btn, + .referral-popup-btn-wrapper .next-btn, + .referral-popup-btn-wrapper .okay-btn { + background-color: var(--specials_buttons-links-and-highlights); + width: 45%; + color: var(--labels_important-active-labels-text-graphics); + border: none; } + .referral-popup-btn-wrapper .okay-btn { + width: 100% !important; } + +.layout-mobile .referral-list-wrapper { + width: 100%; } + .layout-mobile .referral-list-wrapper .earning-discount-label { + font-size: 18px; } + .layout-mobile .referral-list-wrapper .ant-tabs-nav-list { + margin-left: 2rem; } + .layout-mobile .referral-list-wrapper .referral-popup { + display: flex !important; + flex-direction: column !important; + justify-content: space-between !important; } + .layout-mobile .referral-list-wrapper .refer-code-popup-wrapper .unique-referral, + .layout-mobile .referral-list-wrapper .referral-popup-earning-wrapper .unique-referral, + .layout-mobile .referral-list-wrapper .referral-final-popup .unique-referral { + margin-bottom: 15%; } + .layout-mobile .referral-list-wrapper .refer-code-popup-wrapper .referral-label-mobile, + .layout-mobile .referral-list-wrapper .referral-popup-earning-wrapper .referral-label-mobile, + .layout-mobile .referral-list-wrapper .referral-final-popup .referral-label-mobile { + font-size: 14px; } + .layout-mobile .referral-list-wrapper .history-referral-container .referral-history-content { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; } + .layout-mobile .referral-list-wrapper .history-referral-container .events-desc-border { + width: 14%; + border-bottom: 1px solid var(--labels_secondary-inactive-label-text-graphics); } + .layout-mobile .referral-list-wrapper .settle-pop-up .settle { + display: flex !important; + flex-direction: column !important; + justify-content: space-between !important; } + .layout-mobile .referral-list-wrapper .settle-pop-up .settle-popup-wrapper { + margin-bottom: 10%; } + .layout-mobile .referral-list-wrapper .settle-pop-up .settle-popup-wrapper .referral-earning-settlement { + font-size: 18px; } + +.layout-mobile .summary-block_wrapper .create-referral-link { + display: flex; + flex-direction: row; + align-items: flex-start; + justify-content: flex-start; + gap: 2rem; } + +.layout-mobile .summary-block_wrapper .new-refer-mobile-wrapper { + padding: 2rem; } + +.layout-mobile .summary-block_wrapper .referral-active-wrapper { + display: flex; + gap: 1rem; + padding: 1rem; } + +.layout-mobile .summary-block_wrapper .referral-active-text .earn-info-wrapper { + width: 75% !important; } + .layout-mobile .summary-block_wrapper .referral-active-text .earn-info-wrapper .earn-text-wrapper { + justify-content: flex-start; + padding: 5% 0; } + +.layout-mobile .summary-block_wrapper .referral-table-wrapper .table-wrapper th, +.layout-mobile .summary-block_wrapper .referral-table-wrapper tr { + font-size: 12px; } + +.layout-mobile .summary-block_wrapper .referral-table-wrapper .table-wrapper th { + text-wrap: nowrap; } + +.layout-mobile .summary-block_wrapper .referral-table-wrapper td { + padding: 1rem !important; } + +.layout-mobile .summary-block_wrapper .referral-table-wrapper .table_container .table-content { + overflow-x: scroll; } + +.layout-mobile .content-field { + min-height: 20rem !important; } + +@media screen and (max-width: 550px) { + .layout-mobile .icon_title-wrapper { + margin-left: 2rem; } + .layout-mobile .summary-block_wrapper .referral-table-wrapper .table-wrapper th { + padding: 1rem; } } + +@media screen and (max-width: 450px) { + .layout-mobile .summary-block_wrapper .referral-table-wrapper .table-wrapper th { + font-size: 12px; } } + .sidebar-row { min-height: 3rem; min-width: 10rem; } @@ -6659,6 +7594,19 @@ table th { .app_bar .app-bar-account svg .account-inactive { fill: var(--calculated_base_top-bar-navigation_text-inactive); stroke: var(--calculated_base_top-bar-navigation_text-inactive); } + .app_bar .app-bar-account .app-bar-deposit-btn { + color: var(--calculated_base_top-bar-navigation_text-inactive); + background-color: var(--specials_buttons-links-and-highlights); + padding: 2px 10px 0px 10px; + border-radius: 3px; + cursor: pointer; + font-size: 11px; } + .app_bar .app-bar-account .app-bar-deposit-btn .margin-aligner { + margin-bottom: unset; + margin-right: unset; } + .app_bar .app-bar-account .app-bar-deposit-btn .margin-aligner svg { + width: 15px !important; + height: 15px !important; } .app_bar .app_bar-pair-overflow { align-items: center; color: var(--calculated_base_top-bar-navigation_text-inactive); @@ -7366,7 +8314,7 @@ table th { height: 5rem; min-height: 5rem; } .app-menu-bar .app-menu-bar-content { - width: 12rem; } + width: 8rem; } .app-bar-add-tab-menu { width: 24rem; } .app-bar-add-tab-menu.narrow { @@ -8467,7 +9415,9 @@ table th { @media screen and (max-width: 550px) { .field-children .clear-field { - bottom: 1.5rem; } } + bottom: 1.5rem; + padding: 1.5rem; + margin-right: 1rem; } } .mainContainer { display: flex; @@ -9029,11 +9979,12 @@ table th { .layout-mobile .holla-button-font { font-size: 1.2rem; } -.layout-mobile .accordion_wrapper .accordion_section .accordion_section_title { - font-size: 1.75rem; } - -.layout-mobile .accordion_wrapper .accordion_section .accordion_section_content { - font-size: 13px !important; } +.layout-mobile .accordion_wrapper .accordion_section { + border-bottom: unset !important; } + .layout-mobile .accordion_wrapper .accordion_section .accordion_section_title { + font-size: 1.75rem; } + .layout-mobile .accordion_wrapper .accordion_section .accordion_section_content { + font-size: 13px !important; } .layout-mobile .accordion_wrapper .action_notification-svg svg { width: 1.75rem !important; @@ -10040,7 +10991,6 @@ table th { .layout-mobile .ReactModal__Overlay .ReactModal__Content { background-color: var(--base_background) !important; } .layout-mobile .ReactModal__Overlay .ReactModal__Content .dialog-mobile-content { - padding: 2.5rem 0 !important; display: block; } .layout-mobile .trade-details-wrapper { diff --git a/web/src/reducers/appReducer.js b/web/src/reducers/appReducer.js index 11fabc46c7..bfc9143c6c 100644 --- a/web/src/reducers/appReducer.js +++ b/web/src/reducers/appReducer.js @@ -60,6 +60,20 @@ import { SET_TRANSACTION_LIMITS, SET_SELECTED_ACCOUNT, SET_SELECTED_STEP, + SET_WITHDRAW_CURRENCY, + SET_WITHDRAW_NETWORK, + SET_WITHDRAW_ADDRESS, + SET_WITHDRAW_AMOUNT, + SET_WITHDRAW_NETWORK_OPTIONS, + SET_WITHDRAW_FEE, + SET_DEPOSIT_AND_WITHDRAW, + SET_VALID_ADDRESS, + SET_DEPOSIT_NETWORK_OPTIONS, + SET_DEPOSIT_NETWORK, + SET_DEPOSIT_CURRENCY, + SET_SELECTED_METHOD, + SET_RECEIVER_EMAIL, + SET_WITHDRAW_OTIONAL_TAG, } from 'actions/appActions'; import { THEME_DEFAULT } from 'config/constants'; import { getLanguage } from 'utils/string'; @@ -95,6 +109,23 @@ const EMPTY_SNACK_NOTIFICATION = { content: '', isDialog: false, dialogData: [], + timer: 0, +}; + +const WITHDRAW_FIELDS = { + withdrawCurrency: '', + withdrawNetwork: '', + withdrawNetworkOptions: '', + withdrawAddress: '', + withdrawAmount: 0, + withdrawFee: 0, + optionalTag: '', +}; + +const DEPOSIT_FIELDS = { + depositCurrency: '', + depositNetwork: '', + depositNetworkOptions: '', }; const INITIAL_STATE = { @@ -164,6 +195,12 @@ const INITIAL_STATE = { default_digital_assets_sort: DIGITAL_ASSETS_SORT.CHANGE, selectedAccount: 1, selectedStep: 0, + withdrawFields: WITHDRAW_FIELDS, + depositFields: DEPOSIT_FIELDS, + depositAndWithdraw: false, + isValidAddress: '', + selectedWithdrawMethod: 'Address', + receiverWithdrawalEmail: null, }; const reducer = (state = INITIAL_STATE, { type, payload = {} }) => { @@ -286,6 +323,7 @@ const reducer = (state = INITIAL_STATE, { type, payload = {} }) => { icon: payload.icon ? payload.icon : '', useSvg: payload.useSvg ? payload.useSvg : true, content: payload.content ? payload.content : '', + timer: payload.timer ? payload.timer : 0, }, }; @@ -719,6 +757,84 @@ const reducer = (state = INITIAL_STATE, { type, payload = {} }) => { ...state, selectedStep: payload.selectedStep, }; + case SET_WITHDRAW_CURRENCY: + return { + ...state, + withdrawFields: { ...state.withdrawFields, withdrawCurrency: payload }, + }; + case SET_WITHDRAW_NETWORK: + return { + ...state, + withdrawFields: { ...state.withdrawFields, withdrawNetwork: payload }, + }; + case SET_WITHDRAW_NETWORK_OPTIONS: + return { + ...state, + withdrawFields: { + ...state.withdrawFields, + withdrawNetworkOptions: payload, + }, + }; + case SET_WITHDRAW_ADDRESS: + return { + ...state, + withdrawFields: { ...state.withdrawFields, withdrawAddress: payload }, + }; + case SET_WITHDRAW_AMOUNT: + return { + ...state, + withdrawFields: { ...state.withdrawFields, withdrawAmount: payload }, + }; + case SET_WITHDRAW_FEE: + return { + ...state, + withdrawFields: { ...state.withdrawFields, withdrawFee: payload }, + }; + case SET_WITHDRAW_OTIONAL_TAG: + return { + ...state, + withdrawFields: { ...state.withdrawFields, optionalTag: payload }, + }; + case SET_DEPOSIT_AND_WITHDRAW: + return { + ...state, + depositAndWithdraw: payload, + }; + case SET_VALID_ADDRESS: + return { + ...state, + isValidAddress: payload.isValid, + }; + case SET_DEPOSIT_CURRENCY: + return { + ...state, + depositFields: { ...state.depositFields, depositCurrency: payload }, + }; + case SET_DEPOSIT_NETWORK: + return { + ...state, + depositFields: { ...state.depositFields, depositNetwork: payload }, + }; + case SET_DEPOSIT_NETWORK_OPTIONS: + return { + ...state, + depositFields: { + ...state.depositFields, + depositNetworkOptions: payload, + }, + }; + case SET_SELECTED_METHOD: { + return { + ...state, + selectedWithdrawMethod: payload, + }; + } + case SET_RECEIVER_EMAIL: { + return { + ...state, + receiverWithdrawalEmail: payload, + }; + } default: return state; } diff --git a/web/src/reducers/walletReducer.js b/web/src/reducers/walletReducer.js index 0155d26ee3..418b615aed 100644 --- a/web/src/reducers/walletReducer.js +++ b/web/src/reducers/walletReducer.js @@ -44,6 +44,7 @@ const INITIAL_STATE = { depositVerification: INITIAL_VERIFICATION_OBJECT, btcFee: INITIAL_BTC_WHITDRAWALS_FEE, withdrawalCancelData: INITIAL_DELETE_WHITDRAWALS_MSG, + activeTabFromWallet: '', }; export default function reducer(state = INITIAL_STATE, { type, payload }) { @@ -328,6 +329,11 @@ export default function reducer(state = INITIAL_STATE, { type, payload }) { }; case 'LOGOUT': return INITIAL_STATE; + case 'ACTIVE_TAB_FROM_WALLET': + return { + ...state, + activeTabFromWallet: payload.tab, + }; default: return state; } diff --git a/web/src/routes.js b/web/src/routes.js index f8dc69172a..146431e9d5 100644 --- a/web/src/routes.js +++ b/web/src/routes.js @@ -7,6 +7,7 @@ import { App as Container, Account, MainWallet, + P2P, CurrencyWallet, Login, Signup, @@ -67,6 +68,7 @@ import { CoinPage, WhiteLabel, FeesAndLimits, + ReferralList, } from './containers'; import chat from './containers/Admin/Chat'; import { Billing } from 'containers/Admin'; @@ -80,7 +82,6 @@ import { isLoggedIn, getToken, removeToken, - getTokenTimestamp, isAdmin, checkRole, } from './utils/token'; @@ -89,7 +90,6 @@ import { getInterfaceLanguage, getLanguageFromLocal, } from './utils/string'; -import { checkUserSessionExpired } from './utils/utils'; import { getExchangeInitialized, getSetupCompleted } from './utils/initialize'; import PluginConfig from 'containers/Admin/PluginConfig'; import ConfirmChangePassword from 'containers/ConfirmChangePassword'; @@ -120,12 +120,7 @@ if (getLanguageFromLocal()) { let token = getToken(); if (token) { - // check if the token has expired, in that case, remove token - if (checkUserSessionExpired(getTokenTimestamp())) { - removeToken(); - } else { - store.dispatch(verifyToken(token)); - } + store.dispatch(verifyToken(token)); } function requireAuth(nextState, replace) { @@ -396,16 +391,74 @@ export const generateRoutes = (routes = []) => { name="Fees and limits" component={FeesAndLimits} /> + + + + + + + + + + + + + { }; export const roundNumber = (number = 0, decimals = 4) => { - if (number === 0) { + if (number === 0 || number === Infinity || isNaN(number)) { return 0; } else if (decimals > 0) { const multipliedNumber = math.multiply( diff --git a/web/src/utils/token.js b/web/src/utils/token.js index 7570b1e7a0..8af5bc4e0a 100644 --- a/web/src/utils/token.js +++ b/web/src/utils/token.js @@ -20,10 +20,6 @@ export const removeToken = () => { localStorage.removeItem(DASH_TOKEN_TIME_KEY); }; -export const getTokenTimestamp = () => { - return localStorage.getItem(TOKEN_TIME_KEY); -}; - export const isLoggedIn = () => { let token = getToken(); return !!token; diff --git a/web/src/utils/utils.js b/web/src/utils/utils.js index dbac1032e5..63e18d36f3 100644 --- a/web/src/utils/utils.js +++ b/web/src/utils/utils.js @@ -3,7 +3,6 @@ import momentJ from 'moment-jalaali'; import math from 'mathjs'; import _toLower from 'lodash/toLower'; import { - TOKEN_TIME, TIMESTAMP_FORMAT, TIMESTAMP_FORMAT_FA, DEFAULT_TIMESTAMP_FORMAT, @@ -43,12 +42,6 @@ bitcoin.toBTC = (satoshi) => { export default bitcoin; -export const checkUserSessionExpired = (loginTime) => { - const currentTime = Date.now(); - - return currentTime - loginTime > TOKEN_TIME; -}; - export const getFormattedBOD = (date, format = DEFAULT_TIMESTAMP_FORMAT) => { if (getLanguage() === 'fa') { return formatTimestampFarsi(date, format);