From fff9435f73cc3ff90d784e19508837ca5ee54b74 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 29 Jul 2024 15:55:15 +0300 Subject: [PATCH 01/22] transaction.js: remove dependency to util/data.js --- src/transactions/transaction.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/transactions/transaction.js b/src/transactions/transaction.js index e0246c116..d2e9bd151 100644 --- a/src/transactions/transaction.js +++ b/src/transactions/transaction.js @@ -1,5 +1,4 @@ import * as log from '../util/log'; -import { ensureTransaction } from '../util/data'; import * as purchaseProcess from './transactionProcessPurchase'; import * as bookingProcess from './transactionProcessBooking'; import * as inquiryProcess from './transactionProcessInquiry'; @@ -363,9 +362,8 @@ export const TX_TRANSITION_ACTORS = [ * @param {Object} transaction Transaction entity from Marketplace API */ export const getUserTxRole = (currentUserId, transaction) => { - const tx = ensureTransaction(transaction); - const customer = tx.customer; - if (currentUserId && currentUserId.uuid && tx.id && customer.id) { + const customer = transaction?.customer; + if (currentUserId && currentUserId.uuid && transaction?.id && customer.id) { // user can be either customer or provider return currentUserId.uuid === customer.id.uuid ? TX_TRANSITION_ACTOR_CUSTOMER From d737ffb860387ae49f4173fca51c26ef6b24909a Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 29 Jul 2024 16:48:24 +0300 Subject: [PATCH 02/22] Refactor the order of imports a bit --- .../CustomExtendedDataField/CustomExtendedDataField.js | 4 ++-- src/components/FieldCurrencyInput/FieldCurrencyInput.js | 2 +- src/components/LimitedAccessBanner/LimitedAccessBanner.js | 4 +++- .../OrderBreakdown/LineItemProviderCommissionMaybe.js | 3 ++- src/components/OrderPanel/OrderPanel.example.js | 2 +- src/components/PaginationLinks/PaginationLinks.js | 7 ++++--- src/components/ReviewRating/ReviewRating.js | 3 ++- src/components/Reviews/Reviews.js | 6 ++++-- .../PaymentMethodsPage/PaymentMethodsPage.duck.js | 4 ++-- src/ducks/auth.duck.js | 4 ++-- src/ducks/auth.test.js | 2 +- src/ducks/hostedAssets.duck.js | 2 +- src/ducks/paymentMethods.duck.js | 2 +- src/ducks/stripe.duck.js | 2 +- src/ducks/stripeConnectAccount.duck.js | 2 +- src/ducks/user.duck.js | 6 +++--- 16 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/components/CustomExtendedDataField/CustomExtendedDataField.js b/src/components/CustomExtendedDataField/CustomExtendedDataField.js index 8b4f7d0e4..4ba9177c2 100644 --- a/src/components/CustomExtendedDataField/CustomExtendedDataField.js +++ b/src/components/CustomExtendedDataField/CustomExtendedDataField.js @@ -1,6 +1,7 @@ import React from 'react'; // Import config and utils +import { useIntl } from '../../util/reactIntl'; import { SCHEMA_TYPE_ENUM, SCHEMA_TYPE_MULTI_ENUM, @@ -8,10 +9,9 @@ import { SCHEMA_TYPE_LONG, SCHEMA_TYPE_BOOLEAN, } from '../../util/types'; -import { useIntl } from '../../util/reactIntl'; import { required, nonEmptyArray, validateInteger } from '../../util/validators'; // Import shared components -import { FieldCheckboxGroup, FieldSelect, FieldTextInput, FieldBoolean } from '..'; +import { FieldCheckboxGroup, FieldSelect, FieldTextInput, FieldBoolean } from '../../components'; // Import modules from this directory import css from './CustomExtendedDataField.module.css'; diff --git a/src/components/FieldCurrencyInput/FieldCurrencyInput.js b/src/components/FieldCurrencyInput/FieldCurrencyInput.js index ce8b97d21..952415fdf 100644 --- a/src/components/FieldCurrencyInput/FieldCurrencyInput.js +++ b/src/components/FieldCurrencyInput/FieldCurrencyInput.js @@ -20,8 +20,8 @@ import { ensureSeparator, truncateToSubUnitPrecision, } from '../../util/currency'; -import { propTypes } from '../../util/types'; import * as log from '../../util/log'; +import { propTypes } from '../../util/types'; import { ValidationError } from '../../components'; diff --git a/src/components/LimitedAccessBanner/LimitedAccessBanner.js b/src/components/LimitedAccessBanner/LimitedAccessBanner.js index fe2dae54c..da0ce9dff 100644 --- a/src/components/LimitedAccessBanner/LimitedAccessBanner.js +++ b/src/components/LimitedAccessBanner/LimitedAccessBanner.js @@ -1,11 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; + import { FormattedMessage } from '../../util/reactIntl'; import { propTypes } from '../../util/types'; -import { Button } from '../../components'; import { ensureCurrentUser } from '../../util/data'; +import { Button } from '../../components'; + import css from './LimitedAccessBanner.module.css'; // Due to the layout structure, do not render the banner on the following pages diff --git a/src/components/OrderBreakdown/LineItemProviderCommissionMaybe.js b/src/components/OrderBreakdown/LineItemProviderCommissionMaybe.js index f431d3a9a..24a4743c9 100644 --- a/src/components/OrderBreakdown/LineItemProviderCommissionMaybe.js +++ b/src/components/OrderBreakdown/LineItemProviderCommissionMaybe.js @@ -1,8 +1,9 @@ import React from 'react'; import { bool, string } from 'prop-types'; + import { FormattedMessage, intlShape } from '../../util/reactIntl'; -import { formatMoney } from '../../util/currency'; import { types as sdkTypes } from '../../util/sdkLoader'; +import { formatMoney } from '../../util/currency'; import { LINE_ITEM_PROVIDER_COMMISSION, propTypes } from '../../util/types'; import css from './OrderBreakdown.module.css'; diff --git a/src/components/OrderPanel/OrderPanel.example.js b/src/components/OrderPanel/OrderPanel.example.js index d523a7c33..80fc2d8a7 100644 --- a/src/components/OrderPanel/OrderPanel.example.js +++ b/src/components/OrderPanel/OrderPanel.example.js @@ -1,6 +1,6 @@ import React from 'react'; -import { createListing, createUser } from '../../util/testData'; import { LISTING_STATE_CLOSED } from '../../util/types'; +import { createListing, createUser } from '../../util/testData'; import OrderPanel from './OrderPanel'; import css from './OrderPanelExample.module.css'; diff --git a/src/components/PaginationLinks/PaginationLinks.js b/src/components/PaginationLinks/PaginationLinks.js index 508c7d3bf..80aa2c896 100644 --- a/src/components/PaginationLinks/PaginationLinks.js +++ b/src/components/PaginationLinks/PaginationLinks.js @@ -1,11 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { injectIntl, intlShape } from '../../util/reactIntl'; import classNames from 'classnames'; import range from 'lodash/range'; -import { IconArrowHead, NamedLink } from '../../components'; -import { stringify } from '../../util/urlHelpers'; + +import { injectIntl, intlShape } from '../../util/reactIntl'; import { propTypes } from '../../util/types'; +import { stringify } from '../../util/urlHelpers'; +import { IconArrowHead, NamedLink } from '../../components'; import css from './PaginationLinks.module.css'; diff --git a/src/components/ReviewRating/ReviewRating.js b/src/components/ReviewRating/ReviewRating.js index d5349200b..f816baf50 100644 --- a/src/components/ReviewRating/ReviewRating.js +++ b/src/components/ReviewRating/ReviewRating.js @@ -1,9 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { IconReviewStar } from '../../components'; import { REVIEW_RATINGS } from '../../util/types'; +import { IconReviewStar } from '../../components'; + const ReviewRating = props => { const { className, rootClassName, reviewStarClassName, rating } = props; const classes = classNames(rootClassName, className); diff --git a/src/components/Reviews/Reviews.js b/src/components/Reviews/Reviews.js index 03124697b..3bb248233 100644 --- a/src/components/Reviews/Reviews.js +++ b/src/components/Reviews/Reviews.js @@ -1,10 +1,12 @@ import React from 'react'; -import { injectIntl, intlShape } from '../../util/reactIntl'; import { arrayOf, string } from 'prop-types'; import classNames from 'classnames'; -import { Avatar, ReviewRating, UserDisplayName } from '../../components'; + +import { injectIntl, intlShape } from '../../util/reactIntl'; import { propTypes } from '../../util/types'; +import { Avatar, ReviewRating, UserDisplayName } from '../../components'; + import css from './Reviews.module.css'; const Review = props => { diff --git a/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js b/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js index 3634eadef..418542633 100644 --- a/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js +++ b/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js @@ -1,7 +1,7 @@ -import { fetchCurrentUser } from '../../ducks/user.duck'; -import { setInitialValues as setInitialValuesForPaymentMethods } from '../../ducks/paymentMethods.duck'; import { storableError } from '../../util/errors'; import * as log from '../../util/log'; +import { fetchCurrentUser } from '../../ducks/user.duck'; +import { setInitialValues as setInitialValuesForPaymentMethods } from '../../ducks/paymentMethods.duck'; // ================ Action types ================ // diff --git a/src/ducks/auth.duck.js b/src/ducks/auth.duck.js index f195ed053..26b3ad66d 100644 --- a/src/ducks/auth.duck.js +++ b/src/ducks/auth.duck.js @@ -1,7 +1,7 @@ +import * as log from '../util/log'; +import { storableError } from '../util/errors'; import { clearCurrentUser, fetchCurrentUser } from './user.duck'; import { createUserWithIdp } from '../util/api'; -import { storableError } from '../util/errors'; -import * as log from '../util/log'; const authenticated = authInfo => authInfo?.isAnonymous === false; const loggedInAs = authInfo => authInfo?.isLoggedInAs === true; diff --git a/src/ducks/auth.test.js b/src/ducks/auth.test.js index 183de4753..6c3d7256c 100644 --- a/src/ducks/auth.test.js +++ b/src/ducks/auth.test.js @@ -1,3 +1,4 @@ +import * as log from '../util/log'; import { storableError } from '../util/errors'; import { clearCurrentUser, currentUserShowRequest, currentUserShowSuccess } from './user.duck'; import reducer, { @@ -17,7 +18,6 @@ import reducer, { signupError, userLogout, } from './auth.duck'; -import * as log from '../util/log'; // Create a dispatch function that correctly calls the thunk functions // with itself diff --git a/src/ducks/hostedAssets.duck.js b/src/ducks/hostedAssets.duck.js index 13cdb30ef..4ac58d088 100644 --- a/src/ducks/hostedAssets.duck.js +++ b/src/ducks/hostedAssets.duck.js @@ -1,6 +1,6 @@ import { denormalizeAssetData } from '../util/data'; -import { storableError } from '../util/errors'; import * as log from '../util/log'; +import { storableError } from '../util/errors'; // Pick paths from entries of appCdnAssets config (in configDefault.js) const pickHostedConfigPaths = (assetEntries, excludeAssetNames) => { diff --git a/src/ducks/paymentMethods.duck.js b/src/ducks/paymentMethods.duck.js index 633e76657..47739b08a 100644 --- a/src/ducks/paymentMethods.duck.js +++ b/src/ducks/paymentMethods.duck.js @@ -1,6 +1,6 @@ import pick from 'lodash/pick'; -import { storableError } from '../util/errors'; import * as log from '../util/log'; +import { storableError } from '../util/errors'; // ================ Action types ================ // diff --git a/src/ducks/stripe.duck.js b/src/ducks/stripe.duck.js index 0a75a6c23..438721154 100644 --- a/src/ducks/stripe.duck.js +++ b/src/ducks/stripe.duck.js @@ -1,5 +1,5 @@ -import { storableError } from '../util/errors'; import * as log from '../util/log'; +import { storableError } from '../util/errors'; // https://stripe.com/docs/api/payment_intents/object#payment_intent_object-status const STRIPE_PI_HAS_PASSED_CONFIRM = ['processing', 'requires_capture', 'canceled', 'succeeded']; diff --git a/src/ducks/stripeConnectAccount.duck.js b/src/ducks/stripeConnectAccount.duck.js index fe2a3fd46..7515dc786 100644 --- a/src/ducks/stripeConnectAccount.duck.js +++ b/src/ducks/stripeConnectAccount.duck.js @@ -1,7 +1,7 @@ // This file deals with Marketplace API which will create Stripe Custom Connect accounts // from given bank_account tokens. -import { storableError } from '../util/errors'; import * as log from '../util/log'; +import { storableError } from '../util/errors'; // ================ Action types ================ // diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index 2ff93220b..d4303e503 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -1,12 +1,12 @@ +import { util as sdkUtil } from '../util/sdkLoader'; import { denormalisedResponseEntities, ensureOwnListing } from '../util/data'; -import { storableError } from '../util/errors'; -import { LISTING_STATE_DRAFT } from '../util/types'; import * as log from '../util/log'; +import { LISTING_STATE_DRAFT } from '../util/types'; +import { storableError } from '../util/errors'; import { getTransitionsNeedingProviderAttention } from '../transactions/transaction'; import { authInfo } from './auth.duck'; import { stripeAccountCreateSuccess } from './stripeConnectAccount.duck'; -import { util as sdkUtil } from '../util/sdkLoader'; // ================ Action types ================ // From 8873c86a3ee1d9f7dccde9a4ed2f40cd74b95662 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 29 Jul 2024 16:49:57 +0300 Subject: [PATCH 03/22] util/userHelpers: add function to check if user has been approved (state is 'active') --- src/util/userHelpers.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/util/userHelpers.js b/src/util/userHelpers.js index 4d1ea9766..a74c63b04 100644 --- a/src/util/userHelpers.js +++ b/src/util/userHelpers.js @@ -148,3 +148,22 @@ export const hasPermissionToPostListings = currentUser => { } return currentUser?.effectivePermissionSet?.attributes?.postListings === 'permission/allow'; }; + +/** + * Check if currentUser has been approved to gain access. + * I.e. they are not in 'pendig-approval' or 'banned' state. + * + * If the user is in 'pending-approval' state, they don't have right to post listings and initiate transactions. + * User's in 'active' state, they might have right to post listings and initiate transactions. It can be verified by passing permissionsToCheck map. + * + * @param {Object} currentUser API entity. It must have effectivePermissionSet included. + * @param {Object} [permissionsToCheck] E.g. { postListings: true } + * @returns {Boolean} true if currentUser has been approved (state is 'active'). If the _permissionsToCheck_ map is given, those are also checked. + */ +export const isUserAuthorized = (currentUser, permissionsToCheck) => { + const { postListings } = permissionsToCheck || {}; + const isActive = currentUser?.attributes?.state === 'active'; + return permissionsToCheck && postListings + ? isActive && hasPermissionToPostListings(currentUser) + : isActive; +}; From ad589baf26760faecdcbecd5ec455f65991cc3e9 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 29 Jul 2024 16:51:22 +0300 Subject: [PATCH 04/22] ducks/user.duck.js: don't fetch listings or transactions, if user has not been approved --- src/ducks/user.duck.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index d4303e503..98d69a9b3 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -3,6 +3,7 @@ import { denormalisedResponseEntities, ensureOwnListing } from '../util/data'; import * as log from '../util/log'; import { LISTING_STATE_DRAFT } from '../util/types'; import { storableError } from '../util/errors'; +import { isUserAuthorized } from '../util/userHelpers'; import { getTransitionsNeedingProviderAttention } from '../transactions/transaction'; import { authInfo } from './auth.duck'; @@ -315,7 +316,9 @@ export const fetchCurrentUserNotifications = () => (dispatch, getState, sdk) => export const fetchCurrentUser = (params = null) => (dispatch, getState, sdk) => { dispatch(currentUserShowRequest()); - const { isAuthenticated } = getState().auth; + const state = getState(); + const { currentUserHasListings } = state.user || {}; + const { isAuthenticated } = state.auth; if (!isAuthenticated) { // Make sure current user is null @@ -363,10 +366,18 @@ export const fetchCurrentUser = (params = null) => (dispatch, getState, sdk) => return currentUser; }) .then(currentUser => { - dispatch(fetchCurrentUserHasListings()); - dispatch(fetchCurrentUserNotifications()); - if (!currentUser.attributes.emailVerified) { - dispatch(fetchCurrentUserHasOrders()); + // If currentUser is not active (e.g. in 'pending-approval' state), + // then they don't have listings or transactions that we care about. + if (isUserAuthorized(currentUser)) { + if (currentUserHasListings === false) { + dispatch(fetchCurrentUserHasListings()); + } + + dispatch(fetchCurrentUserNotifications()); + + if (!currentUser.attributes.emailVerified) { + dispatch(fetchCurrentUserHasOrders()); + } } // Make sure auth info is up to date From 7e002c70652c9e5a2bb142a22a763aa1e110656c Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 31 Jul 2024 16:51:33 +0300 Subject: [PATCH 05/22] ducks/user.duck.js: avoid double fetching of currentUser on page load --- src/ducks/user.duck.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index 98d69a9b3..c4018e635 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -59,6 +59,7 @@ const mergeCurrentUser = (oldCurrentUser, newCurrentUser) => { const initialState = { currentUser: null, + currentUserShowTimestamp: 0, currentUserShowError: null, currentUserHasListings: false, currentUserHasListingsError: null, @@ -76,7 +77,11 @@ export default function reducer(state = initialState, action = {}) { case CURRENT_USER_SHOW_REQUEST: return { ...state, currentUserShowError: null }; case CURRENT_USER_SHOW_SUCCESS: - return { ...state, currentUser: mergeCurrentUser(state.currentUser, payload) }; + return { + ...state, + currentUser: mergeCurrentUser(state.currentUser, payload), + currentUserShowTimestamp: payload ? new Date().getTime() : 0, + }; case CURRENT_USER_SHOW_ERROR: // eslint-disable-next-line no-console console.error(payload); @@ -315,11 +320,18 @@ export const fetchCurrentUserNotifications = () => (dispatch, getState, sdk) => }; export const fetchCurrentUser = (params = null) => (dispatch, getState, sdk) => { - dispatch(currentUserShowRequest()); const state = getState(); - const { currentUserHasListings } = state.user || {}; + const { currentUserHasListings, currentUserShowTimestamp } = state.user || {}; const { isAuthenticated } = state.auth; + // Double fetch might happen when e.g. profile page is making a full page load + const aSecondAgo = new Date().getTime() - 1000; + if (currentUserShowTimestamp > aSecondAgo) { + return Promise.resolve({}); + } + // Set in-progress, no errors + dispatch(currentUserShowRequest()); + if (!isAuthenticated) { // Make sure current user is null dispatch(currentUserShowSuccess(null)); From 62b39b2a18109d15094d5e70d91fb0e6c9dffdd1 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Tue, 30 Jul 2024 22:06:08 +0300 Subject: [PATCH 06/22] NoAccessPage: add userPendingApproval --- src/containers/NoAccessPage/NoAccessPage.js | 14 ++++++++++++-- src/translations/en.json | 3 +++ src/util/urlHelpers.js | 2 ++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/containers/NoAccessPage/NoAccessPage.js b/src/containers/NoAccessPage/NoAccessPage.js index 547adc6f4..8b3461ed5 100644 --- a/src/containers/NoAccessPage/NoAccessPage.js +++ b/src/containers/NoAccessPage/NoAccessPage.js @@ -6,7 +6,10 @@ import { connect } from 'react-redux'; import { useConfiguration } from '../../context/configurationContext'; import appSettings from '../../config/settings'; import { useIntl } from '../../util/reactIntl'; -import { NO_ACCESS_PAGE_POST_LISTINGS } from '../../util/urlHelpers'; +import { + NO_ACCESS_PAGE_POST_LISTINGS, + NO_ACCESS_PAGE_USER_PENDING_APPROVAL, +} from '../../util/urlHelpers'; import { isScrollingDisabled } from '../../ducks/ui.duck'; import { @@ -32,9 +35,16 @@ export const NoAccessPageComponent = props => { const { scrollingDisabled, params: pathParams } = props; const missingAccessRight = pathParams?.missingAccessRight; + const isUserPendingApprovalPage = missingAccessRight === NO_ACCESS_PAGE_USER_PENDING_APPROVAL; const isPostingRightsPage = missingAccessRight === NO_ACCESS_PAGE_POST_LISTINGS; - const messages = isPostingRightsPage + const messages = isUserPendingApprovalPage + ? { + schemaTitle: 'NoAccessPage.userPendingApproval.schemaTitle', + heading: 'NoAccessPage.userPendingApproval.heading', + content: 'NoAccessPage.userPendingApproval.content', + } + : isPostingRightsPage ? { schemaTitle: 'NoAccessPage.postListings.schemaTitle', heading: 'NoAccessPage.postListings.heading', diff --git a/src/translations/en.json b/src/translations/en.json index f51cf3237..9da048034 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -510,6 +510,9 @@ "NoAccessPage.postListings.schemaTitle": "No publishing rights", "NoAccessPage.postListings.heading": "You can't publish listings", "NoAccessPage.postListings.content": "You need to receive publishing rights from the {marketplaceName} team.", + "NoAccessPage.userPendingApproval.schemaTitle": "No user approval", + "NoAccessPage.userPendingApproval.heading": "Your account is waiting for approval", + "NoAccessPage.userPendingApproval.content": "Your account needs to be approved by the {marketplaceName} team before you can start using it.", "OrderBreakdown.baseUnitDay": "{unitPrice} x {quantity, number} {quantity, plural, one {day} other {days}}", "OrderBreakdown.baseUnitNight": "{unitPrice} x {quantity, number} {quantity, plural, one {night} other {nights}}", "OrderBreakdown.baseUnitHour": "{unitPrice} x {quantity, number} {quantity, plural, one {hour} other {hours}}", diff --git a/src/util/urlHelpers.js b/src/util/urlHelpers.js index 69b124ec9..12199122a 100644 --- a/src/util/urlHelpers.js +++ b/src/util/urlHelpers.js @@ -17,6 +17,8 @@ export const LISTING_PAGE_PARAM_TYPES = [ // No access page - path params: export const NO_ACCESS_PAGE_POST_LISTINGS = 'posting-right'; +// If user account is on pending-approval state, then user can't initiate transactions or create listings +export const NO_ACCESS_PAGE_USER_PENDING_APPROVAL = 'user-approval'; // Create slug from random texts // From Gist thread: https://gist.github.com/mathewbyrne/1280286 From 63134f1f91e0a0ab8976f22dbcdbf4a8cbd239c2 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 31 Jul 2024 16:53:02 +0300 Subject: [PATCH 07/22] ProfilePage.duck.js: user with state pending-approval (no calls to user.show, listings.query, reviews.query) --- .../ProfilePage/ProfilePage.duck.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/containers/ProfilePage/ProfilePage.duck.js b/src/containers/ProfilePage/ProfilePage.duck.js index c48d75691..6e7392262 100644 --- a/src/containers/ProfilePage/ProfilePage.duck.js +++ b/src/containers/ProfilePage/ProfilePage.duck.js @@ -3,6 +3,7 @@ import { fetchCurrentUser } from '../../ducks/user.duck'; import { types as sdkTypes, createImageVariantConfig } from '../../util/sdkLoader'; import { denormalisedResponseEntities } from '../../util/data'; import { storableError } from '../../util/errors'; +import { isUserAuthorized } from '../../util/userHelpers'; const { UUID } = sdkTypes; @@ -188,13 +189,38 @@ export const showUser = (userId, config) => (dispatch, getState, sdk) => { .catch(e => dispatch(showUserError(storableError(e)))); }; +const isCurrentUser = (userId, cu) => userId?.uuid === cu?.id?.uuid; + export const loadData = (params, search, config) => (dispatch, getState, sdk) => { const userId = new UUID(params.id); + const isPreviewForCurrentUser = params.variant === 'pending-approval'; // Clear state so that previously loaded data is not visible // in case this page load fails. dispatch(setInitialState()); + if (isPreviewForCurrentUser) { + return dispatch(fetchCurrentUser()).then(() => { + const currentUser = getState()?.user?.currentUser; + + if (isCurrentUser(userId, currentUser) && isUserAuthorized(currentUser)) { + return Promise.all([ + dispatch(showUser(userId, config)), + dispatch(queryUserListings(userId, config)), + dispatch(queryUserReviews(userId)), + ]); + } else if (isCurrentUser(userId, currentUser)) { + // Handle a scenario, where user (in pending-approval state) + // tries to see their own profile page. + // => just set userId to state + return dispatch(showUserRequest(userId)); + } else { + return; + } + }); + } + + // TODO return Promise.all([ dispatch(fetchCurrentUser()), dispatch(showUser(userId, config)), From 7ab4fb538f93f71f8ad4cada82cba5cb82c20a9f Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 31 Jul 2024 16:55:30 +0300 Subject: [PATCH 08/22] ProfilePage: handle pending-approval state. Use curentUser entity for profile information --- src/components/Avatar/Avatar.js | 18 ++++- .../ProfilePage/ProfilePage.duck.js | 1 - src/containers/ProfilePage/ProfilePage.js | 75 +++++++++++++++---- .../ProfilePage/ProfilePage.module.css | 4 + .../ProfileSettingsPage.js | 37 ++++++--- src/routing/routeConfiguration.js | 7 ++ 6 files changed, 113 insertions(+), 29 deletions(-) diff --git a/src/components/Avatar/Avatar.js b/src/components/Avatar/Avatar.js index 375b1afd7..a6768a692 100644 --- a/src/components/Avatar/Avatar.js +++ b/src/components/Avatar/Avatar.js @@ -9,6 +9,8 @@ import { userDisplayNameAsString, userAbbreviatedName, } from '../../util/data'; +import { isUserAuthorized } from '../../util/userHelpers'; + import { ResponsiveImage, IconBannedUser, NamedLink } from '../../components/'; import css from './Avatar.module.css'; @@ -46,6 +48,10 @@ export const AvatarComponent = props => { const userIsCurrentUser = user && user.type === 'currentUser'; const avatarUser = userIsCurrentUser ? ensureCurrentUser(user) : ensureUser(user); + // I.e. the status is active, not pending-approval or banned + const isUnauthorizedUser = userIsCurrentUser && !isUserAuthorized(user); + const variant = user?.attributes?.state; + //'pending-approval' const isBannedUser = avatarUser.attributes.banned; const isDeletedUser = avatarUser.attributes.deleted; @@ -61,9 +67,15 @@ export const AvatarComponent = props => { const displayName = userDisplayNameAsString(avatarUser, defaultUserDisplayName); const abbreviatedName = userAbbreviatedName(avatarUser, defaultUserAbbreviatedName); const rootProps = { className: classes, title: displayName }; - const linkProps = avatarUser.id - ? { name: 'ProfilePage', params: { id: avatarUser.id.uuid } } - : { name: 'ProfileBasePage' }; + const linkProps = + isUnauthorizedUser && avatarUser.id + ? { + name: 'ProfilePageVariant', + params: { id: avatarUser.id.uuid, variant }, + } + : avatarUser.id + ? { name: 'ProfilePage', params: { id: avatarUser.id.uuid } } + : { name: 'ProfileBasePage' }; const hasProfileImage = avatarUser.profileImage && avatarUser.profileImage.id; const profileLinkEnabled = !disableProfileLink; diff --git a/src/containers/ProfilePage/ProfilePage.duck.js b/src/containers/ProfilePage/ProfilePage.duck.js index 6e7392262..6daaf97f0 100644 --- a/src/containers/ProfilePage/ProfilePage.duck.js +++ b/src/containers/ProfilePage/ProfilePage.duck.js @@ -220,7 +220,6 @@ export const loadData = (params, search, config) => (dispatch, getState, sdk) => }); } - // TODO return Promise.all([ dispatch(fetchCurrentUser()), dispatch(showUser(userId, config)), diff --git a/src/containers/ProfilePage/ProfilePage.js b/src/containers/ProfilePage/ProfilePage.js index 8bcbea7ce..ff313f7db 100644 --- a/src/containers/ProfilePage/ProfilePage.js +++ b/src/containers/ProfilePage/ProfilePage.js @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { bool, arrayOf, number, shape } from 'prop-types'; +import React, { useEffect, useState } from 'react'; +import { bool, arrayOf, oneOfType } from 'prop-types'; import { compose } from 'redux'; import { connect } from 'react-redux'; import classNames from 'classnames'; @@ -13,8 +13,8 @@ import { SCHEMA_TYPE_TEXT, propTypes, } from '../../util/types'; -import { ensureCurrentUser, ensureUser } from '../../util/data'; import { pickCustomFieldProps } from '../../util/fieldHelpers'; +import { isUserAuthorized } from '../../util/userHelpers'; import { richText } from '../../util/richText'; import { isScrollingDisabled } from '../../ducks/ui.duck'; @@ -46,7 +46,7 @@ const MAX_MOBILE_SCREEN_WIDTH = 768; const MIN_LENGTH_FOR_LONG_WORDS = 20; export const AsideContent = props => { - const { user, displayName, isCurrentUser } = props; + const { user, displayName, showLinkToProfileSettingsPage } = props; return (
@@ -55,7 +55,7 @@ export const AsideContent = props => { ) : null} - {isCurrentUser ? ( + {showLinkToProfileSettingsPage ? ( <> @@ -259,7 +259,24 @@ export const MainContent = props => { export const ProfilePageComponent = props => { const config = useConfiguration(); const intl = useIntl(); - const { scrollingDisabled, currentUser, userShowError, user, ...rest } = props; + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const { + scrollingDisabled, + params: pathParams, + currentUser, + useCurrentUser, + userShowError, + user, + ...rest + } = props; + // TODO pending-approval vs 'preview' + const isVariant = pathParams.variant?.length > 0; + const isPreview = isVariant && pathParams.variant === 'pending-approval'; // Stripe's onboarding needs a business URL for each seller, but the profile page can be // too empty for the provider at the time they are creating their first listing. @@ -273,19 +290,37 @@ export const ProfilePageComponent = props => { return ; } - const ensuredCurrentUser = ensureCurrentUser(currentUser); - const profileUser = ensureUser(user); - const isCurrentUser = - ensuredCurrentUser.id && profileUser.id && ensuredCurrentUser.id.uuid === profileUser.id.uuid; + const isCurrentUser = currentUser?.id && currentUser?.id?.uuid === pathParams.id; + const profileUser = useCurrentUser ? currentUser : user; const { bio, displayName, publicData, metadata } = profileUser?.attributes?.profile || {}; const { userFields } = config.user; const schemaTitleVars = { name: displayName, marketplaceName: config.marketplaceName }; const schemaTitle = intl.formatMessage({ id: 'ProfilePage.schemaTitle' }, schemaTitleVars); - if (userShowError && userShowError.status === 404) { + if (!isPreview && userShowError && userShowError.status === 404) { return ; + } else if (isPreview && mounted && !isCurrentUser) { + // Someone is manipulating the URL, redirect to current user's profile page. + return isCurrentUser === false ? ( + + ) : null; + } else if (isPreview && !mounted) { + // This preview of the profile page is not not rendered on server-side + // and the first pass on client-side needs to render the same UI. + return ( + + ); } + // This is rendering normal profile page (not preview for pending-approval) return ( { sideNavClassName={css.aside} topbar={} sideNav={ - + } footer={} > @@ -331,7 +370,8 @@ ProfilePageComponent.defaultProps = { ProfilePageComponent.propTypes = { scrollingDisabled: bool.isRequired, currentUser: propTypes.currentUser, - user: propTypes.user, + useCurrentUser: bool.isRequired, + user: oneOfType([propTypes.user, propTypes.currentUser]), userShowError: propTypes.error, queryListingsError: propTypes.error, listings: arrayOf(propTypes.listing).isRequired, @@ -351,14 +391,19 @@ const mapStateToProps = state => { } = state.ProfilePage; const userMatches = getMarketplaceEntities(state, [{ type: 'user', id: userId }]); const user = userMatches.length === 1 ? userMatches[0] : null; - const listings = getMarketplaceEntities(state, userListingRefs); + + // Show currentUser's data if it's not approved yet + const isCurrentUser = userId?.uuid === currentUser?.id?.uuid; + const useCurrentUser = isCurrentUser && !isUserAuthorized(currentUser); + return { scrollingDisabled: isScrollingDisabled(state), currentUser, + useCurrentUser, user, userShowError, queryListingsError, - listings, + listings: getMarketplaceEntities(state, userListingRefs), reviews, queryReviewsError, }; diff --git a/src/containers/ProfilePage/ProfilePage.module.css b/src/containers/ProfilePage/ProfilePage.module.css index 46e585557..753535c31 100644 --- a/src/containers/ProfilePage/ProfilePage.module.css +++ b/src/containers/ProfilePage/ProfilePage.module.css @@ -40,6 +40,10 @@ margin: 0 96px 48px 0; } } +.avatarPlaceholder { + composes: avatar; + width: 96px; +} .mobileHeading { flex-shrink: 0; diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js index f3f9443a4..fea985adb 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js @@ -7,6 +7,11 @@ import { useConfiguration } from '../../context/configurationContext'; import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl'; import { propTypes } from '../../util/types'; import { ensureCurrentUser } from '../../util/data'; +import { + initialValuesForUserFields, + isUserAuthorized, + pickUserFieldsData, +} from '../../util/userHelpers'; import { isScrollingDisabled } from '../../ducks/ui.duck'; import { H3, Page, UserNav, NamedLink, LayoutSingleColumn } from '../../components'; @@ -18,7 +23,6 @@ import ProfileSettingsForm from './ProfileSettingsForm/ProfileSettingsForm'; import { updateProfile, uploadImage } from './ProfileSettingsPage.duck'; import css from './ProfileSettingsPage.module.css'; -import { initialValuesForUserFields, pickUserFieldsData } from '../../util/userHelpers'; const onImageUploadHandler = (values, fn) => { const { id, imageId, file } = values; @@ -27,6 +31,23 @@ const onImageUploadHandler = (values, fn) => { } }; +const ViewProfileLink = props => { + const { userUUID, isUnauthorizedUser } = props; + return userUUID && isUnauthorizedUser ? ( + + + + ) : userUUID ? ( + + + + ) : null; +}; + export const ProfileSettingsPageComponent = props => { const config = useConfiguration(); const { @@ -90,6 +111,9 @@ export const ProfileSettingsPageComponent = props => { protectedData, privateData, } = user?.attributes.profile; + // I.e. the status is active, not pending-approval or banned + const isUnauthorizedUser = currentUser && !isUserAuthorized(currentUser); + const { userType } = publicData || {}; const profileImageId = user.profileImage ? user.profileImage.id : null; const profileImage = image || { imageId: profileImageId }; @@ -143,15 +167,8 @@ export const ProfileSettingsPageComponent = props => {

- {user.id ? ( - - - - ) : null} + +
{profileSettingsForm} diff --git a/src/routing/routeConfiguration.js b/src/routing/routeConfiguration.js index 74ef8af4d..cda2c118b 100644 --- a/src/routing/routeConfiguration.js +++ b/src/routing/routeConfiguration.js @@ -159,6 +159,13 @@ const routeConfiguration = (layoutConfig) => { component: ProfilePage, loadData: pageDataLoadingAPI.ProfilePage.loadData, }, + { + path: '/u/:id/:variant', + name: 'ProfilePageVariant', + auth: true, + component: ProfilePage, + loadData: pageDataLoadingAPI.ProfilePage.loadData, + }, { path: '/profile-settings', name: 'ProfileSettingsPage', From d93a5a387d28560d870a9a3e23a83770d6986676 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 31 Jul 2024 17:46:04 +0300 Subject: [PATCH 09/22] ListingPage.duck.js: fix inquiry modal state when authentication is needed --- src/containers/ListingPage/ListingPage.duck.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/containers/ListingPage/ListingPage.duck.js b/src/containers/ListingPage/ListingPage.duck.js index 3a271b8fd..de5e1aecc 100644 --- a/src/containers/ListingPage/ListingPage.duck.js +++ b/src/containers/ListingPage/ListingPage.duck.js @@ -125,7 +125,7 @@ const listingPageReducer = (state = initialState, action = {}) => { case SEND_INQUIRY_REQUEST: return { ...state, sendInquiryInProgress: true, sendInquiryError: null }; case SEND_INQUIRY_SUCCESS: - return { ...state, sendInquiryInProgress: false }; + return { ...state, sendInquiryInProgress: false, inquiryModalOpenForListingId: null }; case SEND_INQUIRY_ERROR: return { ...state, sendInquiryInProgress: false, sendInquiryError: payload }; @@ -374,11 +374,12 @@ export const fetchTransactionLineItems = ({ orderData, listingId, isOwnListing } }); }; -export const loadData = (params, search, config) => dispatch => { +export const loadData = (params, search, config) => (dispatch, getState, sdk) => { const listingId = new UUID(params.id); + const inquiryModalOpenForListingId = getState().ListingPage.inquiryModalOpenForListingId; // Clear old line-items - dispatch(setInitialValues({ lineItems: null })); + dispatch(setInitialValues({ lineItems: null, inquiryModalOpenForListingId })); const ownListingVariants = [LISTING_PAGE_DRAFT_VARIANT, LISTING_PAGE_PENDING_APPROVAL_VARIANT]; if (ownListingVariants.includes(params.variant)) { From c967d74fc78874be73b2e578bd39c07f88f948eb Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 31 Jul 2024 18:43:30 +0300 Subject: [PATCH 10/22] ListingPage: don't allow a user in pending-approval state to open inquiry form --- src/containers/ListingPage/ListingPage.duck.js | 7 ++++++- src/containers/ListingPage/ListingPage.shared.js | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/containers/ListingPage/ListingPage.duck.js b/src/containers/ListingPage/ListingPage.duck.js index de5e1aecc..25c4f084a 100644 --- a/src/containers/ListingPage/ListingPage.duck.js +++ b/src/containers/ListingPage/ListingPage.duck.js @@ -7,6 +7,7 @@ import { transactionLineItems } from '../../util/api'; import * as log from '../../util/log'; import { denormalisedResponseEntities } from '../../util/data'; import { findNextBoundary, getStartOf, monthIdString } from '../../util/dates'; +import { isUserAuthorized } from '../../util/userHelpers'; import { LISTING_PAGE_DRAFT_VARIANT, LISTING_PAGE_PENDING_APPROVAL_VARIANT, @@ -376,7 +377,11 @@ export const fetchTransactionLineItems = ({ orderData, listingId, isOwnListing } export const loadData = (params, search, config) => (dispatch, getState, sdk) => { const listingId = new UUID(params.id); - const inquiryModalOpenForListingId = getState().ListingPage.inquiryModalOpenForListingId; + const state = getState(); + const currentUser = state.user?.currentUser; + const inquiryModalOpenForListingId = isUserAuthorized(currentUser) + ? state.ListingPage.inquiryModalOpenForListingId + : null; // Clear old line-items dispatch(setInitialValues({ lineItems: null, inquiryModalOpenForListingId })); diff --git a/src/containers/ListingPage/ListingPage.shared.js b/src/containers/ListingPage/ListingPage.shared.js index 31a9f6b63..478e5cc17 100644 --- a/src/containers/ListingPage/ListingPage.shared.js +++ b/src/containers/ListingPage/ListingPage.shared.js @@ -4,7 +4,8 @@ import { types as sdkTypes } from '../../util/sdkLoader'; import { createResourceLocatorString, findRouteByRouteName } from '../../util/routes'; import { formatMoney } from '../../util/currency'; import { timestampToDate } from '../../util/dates'; -import { createSlug } from '../../util/urlHelpers'; +import { isUserAuthorized } from '../../util/userHelpers'; +import { NO_ACCESS_PAGE_USER_PENDING_APPROVAL, createSlug } from '../../util/urlHelpers'; import { Page, LayoutSingleColumn } from '../../components'; import FooterContainer from '../../containers/FooterContainer/FooterContainer'; @@ -98,6 +99,10 @@ export const handleContactUser = parameters => () => { // signup and return back to listingPage. history.push(createResourceLocatorString('SignupPage', routes, {}, {}), state); + } else if (!isUserAuthorized(currentUser)) { + // A user in pending-approval state can't contact the author (the same applies for a banned user) + const pathParams = { missingAccessRight: NO_ACCESS_PAGE_USER_PENDING_APPROVAL }; + history.push(createResourceLocatorString('NoAccessPage', routes, pathParams, {})); } else { setInquiryModalOpen(true); } From 788292c24f9728677444c07695889211f65fb006 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 31 Jul 2024 18:45:21 +0300 Subject: [PATCH 11/22] ListingPage/InquiryForm: last line of defense against pending-approval access control wall. User should not end up to inquiry form, but if there's a bug, this should allow user to copy the message. --- src/containers/ListingPage/InquiryForm/InquiryForm.js | 9 ++++++++- src/translations/en.json | 1 + src/util/errors.js | 10 ++++++++++ src/util/types.js | 2 ++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/containers/ListingPage/InquiryForm/InquiryForm.js b/src/containers/ListingPage/InquiryForm/InquiryForm.js index a9b2f549b..b7ae98e04 100644 --- a/src/containers/ListingPage/InquiryForm/InquiryForm.js +++ b/src/containers/ListingPage/InquiryForm/InquiryForm.js @@ -7,7 +7,10 @@ import classNames from 'classnames'; import { FormattedMessage, injectIntl, intlShape } from '../../../util/reactIntl'; import * as validators from '../../../util/validators'; import { propTypes } from '../../../util/types'; -import { isTooManyRequestsError } from '../../../util/errors'; +import { + isErrorNoPermissionForUserPendingApproval, + isTooManyRequestsError, +} from '../../../util/errors'; import { Form, PrimaryButton, FieldTextInput, IconInquiry, Heading } from '../../../components'; @@ -15,6 +18,8 @@ import css from './InquiryForm.module.css'; const ErrorMessage = props => { const { error } = props; + const userPendingApproval = true || isErrorNoPermissionForUserPendingApproval(error); + // No transaction process attached to listing return error ? (

@@ -22,6 +27,8 @@ const ErrorMessage = props => { ) : isTooManyRequestsError(error) ? ( + ) : userPendingApproval ? ( + ) : ( )} diff --git a/src/translations/en.json b/src/translations/en.json index 9da048034..7b1550163 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -400,6 +400,7 @@ "InquiryForm.sendInquiryErrorNoProcess": "Oops, no transaction process attached to the listing. Please contact support", "InquiryForm.submitButtonText": "Send inquiry", "InquiryForm.tooManyRequestsError": "Sending inquiry failed. There have been too many requests made in a short amount of time. If the error persists, try refreshing the page or contact support.", + "InquiryForm.userPendingApprovalError": "Oops, something went wrong. You don't have permission to make inquiries", "InquiryWithoutPaymentForm.ctaButton": "Send an inquiry", "KeywordFilter.filterText": "Filter results by", "KeywordFilter.labelSelected": "\"{labelText}\"", diff --git a/src/util/errors.js b/src/util/errors.js index 6ff1f221c..45b0a0be2 100644 --- a/src/util/errors.js +++ b/src/util/errors.js @@ -25,6 +25,7 @@ import { ERROR_CODE_TRANSACTION_LISTING_INSUFFICIENT_STOCK, ERROR_CODE_STOCK_OLD_TOTAL_MISMATCH, ERROR_CODE_PERMISSION_DENIED_POST_LISTINGS, + ERROR_CODE_PERMISSION_DENIED_PENDING_APPROVAL, } from './types'; // NOTE: This file imports types.js, which may lead to circular dependency @@ -246,6 +247,15 @@ export const isErrorNoPermissionToPostListings = error => error.status === 403 && hasErrorWithCode(error, ERROR_CODE_PERMISSION_DENIED_POST_LISTINGS); +/** + * Check if the given API error (from `sdk.transactions.initiate(params)` + * is due to denied permission for users in pending-approval state. + */ +export const isErrorNoPermissionForUserPendingApproval = error => + error && + error.status === 403 && + hasErrorWithCode(error, ERROR_CODE_PERMISSION_DENIED_PENDING_APPROVAL); + /** * Check if the given API error (from * 'sdk.stripeAccount.create(payoutDetails)') is due to diff --git a/src/util/types.js b/src/util/types.js index 75d06772a..3164b0b0a 100644 --- a/src/util/types.js +++ b/src/util/types.js @@ -629,6 +629,7 @@ export const ERROR_CODE_FORBIDDEN = 'forbidden'; export const ERROR_CODE_MISSING_STRIPE_ACCOUNT = 'transaction-missing-stripe-account'; export const ERROR_CODE_STOCK_OLD_TOTAL_MISMATCH = 'old-total-mismatch'; export const ERROR_CODE_PERMISSION_DENIED_POST_LISTINGS = 'permission-denied-post-listings'; +export const ERROR_CODE_PERMISSION_DENIED_PENDING_APPROVAL = 'permission-denied-pending-approval'; const ERROR_CODES = [ ERROR_CODE_TRANSACTION_LISTING_NOT_FOUND, @@ -649,6 +650,7 @@ const ERROR_CODES = [ ERROR_CODE_MISSING_STRIPE_ACCOUNT, ERROR_CODE_STOCK_OLD_TOTAL_MISMATCH, ERROR_CODE_PERMISSION_DENIED_POST_LISTINGS, + ERROR_CODE_PERMISSION_DENIED_PENDING_APPROVAL, ]; // API error From 598624a43cb906d75ee28359eed0a3e145590827 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 31 Jul 2024 18:49:25 +0300 Subject: [PATCH 12/22] EditListingPage: don't allow a user in pending-approval state to open EditListingWizard --- .../EditListingPage/EditListingPage.duck.js | 25 +++++++++++-------- .../EditListingPage/EditListingPage.js | 12 +++++++-- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/containers/EditListingPage/EditListingPage.duck.js b/src/containers/EditListingPage/EditListingPage.duck.js index 46a49af30..3db0862f7 100644 --- a/src/containers/EditListingPage/EditListingPage.duck.js +++ b/src/containers/EditListingPage/EditListingPage.duck.js @@ -14,6 +14,7 @@ import { uniqueBy } from '../../util/generators'; import { storableError } from '../../util/errors'; import * as log from '../../util/log'; import { parse } from '../../util/urlHelpers'; +import { isUserAuthorized } from '../../util/userHelpers'; import { isBookingProcessAlias } from '../../transactions/transaction'; import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck'; @@ -936,17 +937,21 @@ export const loadData = (params, search, config) => (dispatch, getState, sdk) => return Promise.all([dispatch(requestShowListing(payload, config)), dispatch(fetchCurrentUser())]) .then(response => { const currentUser = getState().user.currentUser; - if (currentUser && currentUser.stripeAccount) { - dispatch(fetchStripeAccount()); - } - // Because of two dispatch functions, response is an array. - // We are only interested in the response from requestShowListing here, - // so we need to pick the first one - const listing = response[0]?.data?.data; - const transactionProcessAlias = listing?.attributes?.publicData?.transactionProcessAlias; - if (listing && isBookingProcessAlias(transactionProcessAlias)) { - fetchLoadDataExceptions(dispatch, listing, search, config.localization.firstDayOfWeek); + // Do not fetch extra information if user is in pending-approval state. + if (isUserAuthorized(currentUser)) { + if (currentUser && currentUser.stripeAccount) { + dispatch(fetchStripeAccount()); + } + + // Because of two dispatch functions, response is an array. + // We are only interested in the response from requestShowListing here, + // so we need to pick the first one + const listing = response[0]?.data?.data; + const transactionProcessAlias = listing?.attributes?.publicData?.transactionProcessAlias; + if (listing && isBookingProcessAlias(transactionProcessAlias)) { + fetchLoadDataExceptions(dispatch, listing, search, config.localization.firstDayOfWeek); + } } return response; diff --git a/src/containers/EditListingPage/EditListingPage.js b/src/containers/EditListingPage/EditListingPage.js index c144eedce..e1776d134 100644 --- a/src/containers/EditListingPage/EditListingPage.js +++ b/src/containers/EditListingPage/EditListingPage.js @@ -13,6 +13,7 @@ import { LISTING_PAGE_PARAM_TYPES, LISTING_PAGE_PENDING_APPROVAL_VARIANT, NO_ACCESS_PAGE_POST_LISTINGS, + NO_ACCESS_PAGE_USER_PENDING_APPROVAL, createSlug, parse, } from '../../util/urlHelpers'; @@ -20,7 +21,7 @@ import { import { LISTING_STATE_DRAFT, LISTING_STATE_PENDING_APPROVAL, propTypes } from '../../util/types'; import { isErrorNoPermissionToPostListings } from '../../util/errors'; import { ensureOwnListing } from '../../util/data'; -import { hasPermissionToPostListings } from '../../util/userHelpers'; +import { hasPermissionToPostListings, isUserAuthorized } from '../../util/userHelpers'; import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck'; import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/ui.duck'; import { @@ -138,7 +139,14 @@ export const EditListingPageComponent = props => { const hasStripeOnboardingDataIfNeeded = returnURLType ? !!currentUser?.id : true; const showWizard = hasStripeOnboardingDataIfNeeded && (isNewURI || currentListing.id); - if (shouldRedirectNoPostingRights) { + if (!isUserAuthorized(currentUser)) { + return ( + + ); + } else if (shouldRedirectNoPostingRights) { return ( Date: Wed, 31 Jul 2024 18:50:53 +0300 Subject: [PATCH 13/22] CheckoutPage: don't allow a user in pending-approval state to open CheckoutPage --- src/containers/CheckoutPage/CheckoutPage.js | 32 +++++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/containers/CheckoutPage/CheckoutPage.js b/src/containers/CheckoutPage/CheckoutPage.js index 2a284a692..1b90cd97e 100644 --- a/src/containers/CheckoutPage/CheckoutPage.js +++ b/src/containers/CheckoutPage/CheckoutPage.js @@ -8,6 +8,8 @@ import { useIntl } from 'react-intl'; import { useConfiguration } from '../../context/configurationContext'; import { useRouteConfiguration } from '../../context/routeConfigurationContext'; import { userDisplayNameAsString } from '../../util/data'; +import { NO_ACCESS_PAGE_USER_PENDING_APPROVAL } from '../../util/urlHelpers'; +import { isUserAuthorized } from '../../util/userHelpers'; import { INQUIRY_PROCESS_NAME, resolveLatestProcessName } from '../../transactions/transaction'; // Import global thunk functions @@ -64,6 +66,7 @@ const EnhancedCheckoutPage = props => { useEffect(() => { const { + currentUser, orderData, listing, transaction, @@ -75,15 +78,18 @@ const EnhancedCheckoutPage = props => { setPageData(data || {}); setIsDataLoaded(true); - // This is for processes using payments with Stripe integration - if (getProcessName(data) !== INQUIRY_PROCESS_NAME) { - // Fetch StripeCustomer and speculateTransition for transactions that include Stripe payments - loadInitialDataForStripePayments({ - pageData: data || {}, - fetchSpeculatedTransaction, - fetchStripeCustomer, - config, - }); + // Do not fetch extra data if user is not active (E.g. they are in pending-approval state.) + if (isUserAuthorized(currentUser)) { + // This is for processes using payments with Stripe integration + if (getProcessName(data) !== INQUIRY_PROCESS_NAME) { + // Fetch StripeCustomer and speculateTransition for transactions that include Stripe payments + loadInitialDataForStripePayments({ + pageData: data || {}, + fetchSpeculatedTransaction, + fetchStripeCustomer, + config, + }); + } } }, []); @@ -102,6 +108,7 @@ const EnhancedCheckoutPage = props => { const isOwnListing = currentUser?.id && listing?.author?.id?.uuid === currentUser?.id?.uuid; const hasRequiredData = !!(listing?.id && listing?.author?.id && processName); const shouldRedirect = isDataLoaded && !(hasRequiredData && !isOwnListing); + const shouldRedirectUnathorizedUser = isDataLoaded && !isUserAuthorized(currentUser); // Redirect back to ListingPage if data is missing. // Redirection must happen before any data format error is thrown (e.g. wrong currency) @@ -111,6 +118,13 @@ const EnhancedCheckoutPage = props => { listing, }); return ; + } else if (shouldRedirectUnathorizedUser) { + return ( + + ); } const listingTitle = listing?.attributes?.title; From 7a4b0593c226de1b25e4f1554eddcab4d3c416f0 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Thu, 1 Aug 2024 13:58:53 +0300 Subject: [PATCH 14/22] Remove wrongly copy-pasted currentUserHasListings and currentUserHasOrders --- src/containers/AuthenticationPage/AuthenticationPage.test.js | 1 - src/containers/ContactDetailsPage/ContactDetailsPage.test.js | 1 - src/containers/EditListingPage/EditListingPage.js | 1 - src/containers/EditListingPage/EditListingPage.test.js | 1 - src/containers/InboxPage/InboxPage.js | 1 - src/containers/PasswordChangePage/PasswordChangePage.test.js | 1 - src/containers/PasswordRecoveryPage/PasswordRecoveryPage.test.js | 1 - src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js | 1 - 8 files changed, 8 deletions(-) diff --git a/src/containers/AuthenticationPage/AuthenticationPage.test.js b/src/containers/AuthenticationPage/AuthenticationPage.test.js index fd43613f8..62980cf9d 100644 --- a/src/containers/AuthenticationPage/AuthenticationPage.test.js +++ b/src/containers/AuthenticationPage/AuthenticationPage.test.js @@ -15,7 +15,6 @@ const props = { isAuthenticated: false, authInProgress: false, scrollingDisabled: false, - currentUserHasListings: false, onLogout: noop, onManageDisableScrolling: noop, onResendVerificationEmail: noop, diff --git a/src/containers/ContactDetailsPage/ContactDetailsPage.test.js b/src/containers/ContactDetailsPage/ContactDetailsPage.test.js index ad05f6f72..f45467015 100644 --- a/src/containers/ContactDetailsPage/ContactDetailsPage.test.js +++ b/src/containers/ContactDetailsPage/ContactDetailsPage.test.js @@ -21,7 +21,6 @@ describe('ContactDetailsPageComponent', () => { scrollingDisabled={false} authInProgress={false} currentUser={createCurrentUser('user1')} - currentUserHasListings={false} isAuthenticated={false} onChange={noop} onLogout={noop} diff --git a/src/containers/EditListingPage/EditListingPage.js b/src/containers/EditListingPage/EditListingPage.js index e1776d134..65d5e50e9 100644 --- a/src/containers/EditListingPage/EditListingPage.js +++ b/src/containers/EditListingPage/EditListingPage.js @@ -297,7 +297,6 @@ EditListingPageComponent.defaultProps = { stripeAccountFetched: null, currentUser: null, stripeAccount: null, - currentUserHasOrders: null, listing: null, listingDraft: null, notificationCount: 0, diff --git a/src/containers/EditListingPage/EditListingPage.test.js b/src/containers/EditListingPage/EditListingPage.test.js index a7f4d7b6a..dfbde0663 100644 --- a/src/containers/EditListingPage/EditListingPage.test.js +++ b/src/containers/EditListingPage/EditListingPage.test.js @@ -2352,7 +2352,6 @@ describe('EditListingPageComponent', () => { render( { InboxPageComponent.defaultProps = { currentUser: null, - currentUserHasOrders: null, fetchOrdersOrSalesError: null, pagination: null, providerNotificationCount: 0, diff --git a/src/containers/PasswordChangePage/PasswordChangePage.test.js b/src/containers/PasswordChangePage/PasswordChangePage.test.js index ca958df17..1eda4b83e 100644 --- a/src/containers/PasswordChangePage/PasswordChangePage.test.js +++ b/src/containers/PasswordChangePage/PasswordChangePage.test.js @@ -20,7 +20,6 @@ describe('PasswordChangePageComponent', () => { scrollingDisabled={false} authInProgress={false} currentUser={createCurrentUser('user1')} - currentUserHasListings={false} isAuthenticated={false} onChange={noop} onLogout={noop} diff --git a/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.test.js b/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.test.js index fe67d5975..b63f9e5b9 100644 --- a/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.test.js +++ b/src/containers/PasswordRecoveryPage/PasswordRecoveryPage.test.js @@ -19,7 +19,6 @@ describe('PasswordRecoveryPageComponent', () => { location={{ search: '' }} scrollingDisabled={false} authInProgress={false} - currentUserHasListings={false} isAuthenticated={false} onLogout={noop} onManageDisableScrolling={noop} diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js index 23895f6e8..8e8a848b4 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.test.js @@ -15,7 +15,6 @@ describe('ProfileSettingsPage', () => { const props = { authInProgress: false, currentUser: createCurrentUser('userId'), - currentUserHasListings: false, history: { push: noop }, isAuthenticated: false, location: { search: '' }, From 06e25726a1e403749570006e56d5f0bb64a8ddb2 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Thu, 1 Aug 2024 14:05:59 +0300 Subject: [PATCH 15/22] user.duck: wrap current parameter inside options object fetchCurrentUser({ callParams }); --- src/containers/CheckoutPage/CheckoutPage.duck.js | 5 ++++- src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js | 5 ++++- src/ducks/user.duck.js | 5 +++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/containers/CheckoutPage/CheckoutPage.duck.js b/src/containers/CheckoutPage/CheckoutPage.duck.js index b6e5e33b6..eafac96bc 100644 --- a/src/containers/CheckoutPage/CheckoutPage.duck.js +++ b/src/containers/CheckoutPage/CheckoutPage.duck.js @@ -478,8 +478,11 @@ export const speculateTransaction = ( // We need to fetch currentUser with correct params to include relationship export const stripeCustomer = () => (dispatch, getState, sdk) => { dispatch(stripeCustomerRequest()); + const fetchCurrentUserOptions = { + callParams: { include: ['stripeCustomer.defaultPaymentMethod'] }, + }; - return dispatch(fetchCurrentUser({ include: ['stripeCustomer.defaultPaymentMethod'] })) + return dispatch(fetchCurrentUser(fetchCurrentUserOptions)) .then(response => { dispatch(stripeCustomerSuccess()); }) diff --git a/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js b/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js index 418542633..defd376e8 100644 --- a/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js +++ b/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js @@ -87,8 +87,11 @@ export const createStripeSetupIntent = () => (dispatch, getState, sdk) => { export const stripeCustomer = () => (dispatch, getState, sdk) => { dispatch(stripeCustomerRequest()); + const fetchCurrentUserOptions = { + callParams: { include: ['stripeCustomer.defaultPaymentMethod'] }, + }; - return dispatch(fetchCurrentUser({ include: ['stripeCustomer.defaultPaymentMethod'] })) + return dispatch(fetchCurrentUser(fetchCurrentUserOptions)) .then(response => { dispatch(stripeCustomerSuccess()); }) diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index c4018e635..fe6a5be73 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -319,10 +319,11 @@ export const fetchCurrentUserNotifications = () => (dispatch, getState, sdk) => .catch(e => dispatch(fetchCurrentUserNotificationsError(storableError(e)))); }; -export const fetchCurrentUser = (params = null) => (dispatch, getState, sdk) => { +export const fetchCurrentUser = options => (dispatch, getState, sdk) => { const state = getState(); const { currentUserHasListings, currentUserShowTimestamp } = state.user || {}; const { isAuthenticated } = state.auth; + const { callParams = null } = options || {}; // Double fetch might happen when e.g. profile page is making a full page load const aSecondAgo = new Date().getTime() - 1000; @@ -338,7 +339,7 @@ export const fetchCurrentUser = (params = null) => (dispatch, getState, sdk) => return Promise.resolve({}); } - const parameters = params || { + const parameters = callParams || { include: ['effectivePermissionSet', 'profileImage', 'stripeAccount'], 'fields.image': [ 'variants.square-small', From d64ede7274c0ec9006e88ff034dfafec54288185 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Thu, 1 Aug 2024 16:18:34 +0300 Subject: [PATCH 16/22] user.duck: fetchCurrentUser should not always fetch listings and transactions. This adds options to by-pass those extra calls. Note: at some point we want to get rid off these altogether and get that aggregated info from currentUser call. --- src/containers/CheckoutPage/CheckoutPage.duck.js | 2 ++ src/containers/EditListingPage/EditListingPage.duck.js | 10 ++++++++-- src/containers/ListingPage/ListingPage.duck.js | 8 +++++++- .../PaymentMethodsPage/PaymentMethodsPage.duck.js | 2 ++ src/containers/ProfilePage/ProfilePage.duck.js | 8 ++++++-- .../StripePayoutPage/StripePayoutPage.duck.js | 6 +++++- src/ducks/user.duck.js | 8 +++++--- 7 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/containers/CheckoutPage/CheckoutPage.duck.js b/src/containers/CheckoutPage/CheckoutPage.duck.js index eafac96bc..d548cd71e 100644 --- a/src/containers/CheckoutPage/CheckoutPage.duck.js +++ b/src/containers/CheckoutPage/CheckoutPage.duck.js @@ -480,6 +480,8 @@ export const stripeCustomer = () => (dispatch, getState, sdk) => { dispatch(stripeCustomerRequest()); const fetchCurrentUserOptions = { callParams: { include: ['stripeCustomer.defaultPaymentMethod'] }, + updateHasListings: false, + updateNotifications: false, }; return dispatch(fetchCurrentUser(fetchCurrentUserOptions)) diff --git a/src/containers/EditListingPage/EditListingPage.duck.js b/src/containers/EditListingPage/EditListingPage.duck.js index 3db0862f7..867d8a0e4 100644 --- a/src/containers/EditListingPage/EditListingPage.duck.js +++ b/src/containers/EditListingPage/EditListingPage.duck.js @@ -917,10 +917,13 @@ export const loadData = (params, search, config) => (dispatch, getState, sdk) => dispatch(clearUpdatedTab()); dispatch(clearPublishError()); const { id, type } = params; + const fetchCurrentUserOptions = { + updateNotifications: false, + }; if (type === 'new') { // No need to listing data when creating a new listing - return Promise.all([dispatch(fetchCurrentUser())]) + return Promise.all([dispatch(fetchCurrentUser(fetchCurrentUserOptions))]) .then(response => { const currentUser = getState().user.currentUser; if (currentUser && currentUser.stripeAccount) { @@ -934,7 +937,10 @@ export const loadData = (params, search, config) => (dispatch, getState, sdk) => } const payload = { id: new UUID(id) }; - return Promise.all([dispatch(requestShowListing(payload, config)), dispatch(fetchCurrentUser())]) + return Promise.all([ + dispatch(requestShowListing(payload, config)), + dispatch(fetchCurrentUser(fetchCurrentUserOptions)), + ]) .then(response => { const currentUser = getState().user.currentUser; diff --git a/src/containers/ListingPage/ListingPage.duck.js b/src/containers/ListingPage/ListingPage.duck.js index 25c4f084a..ea57bb9b4 100644 --- a/src/containers/ListingPage/ListingPage.duck.js +++ b/src/containers/ListingPage/ListingPage.duck.js @@ -203,7 +203,13 @@ export const showListing = (listingId, config, isOwn = false) => (dispatch, getS const aspectRatio = aspectHeight / aspectWidth; dispatch(showListingRequest(listingId)); - dispatch(fetchCurrentUser()); + // Current user entity is fetched in a bit lazy fashion, since it's not tied to returned Promise chain. + const fetchCurrentUserOptions = { + updateHasListings: false, + updateNotifications: false, + }; + dispatch(fetchCurrentUser(fetchCurrentUserOptions)); + const params = { id: listingId, include: ['author', 'author.profileImage', 'images', 'currentStock'], diff --git a/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js b/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js index defd376e8..c5222be13 100644 --- a/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js +++ b/src/containers/PaymentMethodsPage/PaymentMethodsPage.duck.js @@ -89,6 +89,8 @@ export const stripeCustomer = () => (dispatch, getState, sdk) => { dispatch(stripeCustomerRequest()); const fetchCurrentUserOptions = { callParams: { include: ['stripeCustomer.defaultPaymentMethod'] }, + updateHasListings: false, + updateNotifications: false, }; return dispatch(fetchCurrentUser(fetchCurrentUserOptions)) diff --git a/src/containers/ProfilePage/ProfilePage.duck.js b/src/containers/ProfilePage/ProfilePage.duck.js index 6daaf97f0..6dc723b8a 100644 --- a/src/containers/ProfilePage/ProfilePage.duck.js +++ b/src/containers/ProfilePage/ProfilePage.duck.js @@ -194,13 +194,17 @@ const isCurrentUser = (userId, cu) => userId?.uuid === cu?.id?.uuid; export const loadData = (params, search, config) => (dispatch, getState, sdk) => { const userId = new UUID(params.id); const isPreviewForCurrentUser = params.variant === 'pending-approval'; + const fetchCurrentUserOptions = { + updateHasListings: false, + updateNotifications: false, + }; // Clear state so that previously loaded data is not visible // in case this page load fails. dispatch(setInitialState()); if (isPreviewForCurrentUser) { - return dispatch(fetchCurrentUser()).then(() => { + return dispatch(fetchCurrentUser(fetchCurrentUserOptions)).then(() => { const currentUser = getState()?.user?.currentUser; if (isCurrentUser(userId, currentUser) && isUserAuthorized(currentUser)) { @@ -221,7 +225,7 @@ export const loadData = (params, search, config) => (dispatch, getState, sdk) => } return Promise.all([ - dispatch(fetchCurrentUser()), + dispatch(fetchCurrentUser(fetchCurrentUserOptions)), dispatch(showUser(userId, config)), dispatch(queryUserListings(userId, config)), dispatch(queryUserReviews(userId)), diff --git a/src/containers/StripePayoutPage/StripePayoutPage.duck.js b/src/containers/StripePayoutPage/StripePayoutPage.duck.js index 4647f4ffc..52082e71e 100644 --- a/src/containers/StripePayoutPage/StripePayoutPage.duck.js +++ b/src/containers/StripePayoutPage/StripePayoutPage.duck.js @@ -74,8 +74,12 @@ export const loadData = () => (dispatch, getState, sdk) => { // Clear state so that previously loaded data is not visible // in case this page load fails. dispatch(setInitialValues()); + const fetchCurrentUserOptions = { + updateHasListings: false, + updateNotifications: false, + }; - return dispatch(fetchCurrentUser()).then(response => { + return dispatch(fetchCurrentUser(fetchCurrentUserOptions)).then(response => { const currentUser = getState().user.currentUser; if (currentUser && currentUser.stripeAccount) { dispatch(fetchStripeAccount()); diff --git a/src/ducks/user.duck.js b/src/ducks/user.duck.js index fe6a5be73..90b9ec953 100644 --- a/src/ducks/user.duck.js +++ b/src/ducks/user.duck.js @@ -323,7 +323,7 @@ export const fetchCurrentUser = options => (dispatch, getState, sdk) => { const state = getState(); const { currentUserHasListings, currentUserShowTimestamp } = state.user || {}; const { isAuthenticated } = state.auth; - const { callParams = null } = options || {}; + const { callParams = null, updateHasListings = true, updateNotifications = true } = options || {}; // Double fetch might happen when e.g. profile page is making a full page load const aSecondAgo = new Date().getTime() - 1000; @@ -382,11 +382,13 @@ export const fetchCurrentUser = options => (dispatch, getState, sdk) => { // If currentUser is not active (e.g. in 'pending-approval' state), // then they don't have listings or transactions that we care about. if (isUserAuthorized(currentUser)) { - if (currentUserHasListings === false) { + if (currentUserHasListings === false && updateHasListings !== false) { dispatch(fetchCurrentUserHasListings()); } - dispatch(fetchCurrentUserNotifications()); + if (updateNotifications !== false) { + dispatch(fetchCurrentUserNotifications()); + } if (!currentUser.attributes.emailVerified) { dispatch(fetchCurrentUserHasOrders()); From 02832cbd76e72cb9975f7e21854a1b594725281f Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 5 Aug 2024 18:22:40 +0300 Subject: [PATCH 17/22] ProfilePage: add PROFILE_PAGE_PENDING_APPROVAL_VARIANT url helper --- src/containers/ProfilePage/ProfilePage.duck.js | 3 ++- src/containers/ProfilePage/ProfilePage.js | 4 ++-- src/containers/ProfilePage/ProfilePage.test.js | 1 + src/containers/ProfileSettingsPage/ProfileSettingsPage.js | 3 ++- src/util/urlHelpers.js | 2 ++ 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/containers/ProfilePage/ProfilePage.duck.js b/src/containers/ProfilePage/ProfilePage.duck.js index 6dc723b8a..d06f842d4 100644 --- a/src/containers/ProfilePage/ProfilePage.duck.js +++ b/src/containers/ProfilePage/ProfilePage.duck.js @@ -1,6 +1,7 @@ import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck'; import { fetchCurrentUser } from '../../ducks/user.duck'; import { types as sdkTypes, createImageVariantConfig } from '../../util/sdkLoader'; +import { PROFILE_PAGE_PENDING_APPROVAL_VARIANT } from '../../util/urlHelpers'; import { denormalisedResponseEntities } from '../../util/data'; import { storableError } from '../../util/errors'; import { isUserAuthorized } from '../../util/userHelpers'; @@ -193,7 +194,7 @@ const isCurrentUser = (userId, cu) => userId?.uuid === cu?.id?.uuid; export const loadData = (params, search, config) => (dispatch, getState, sdk) => { const userId = new UUID(params.id); - const isPreviewForCurrentUser = params.variant === 'pending-approval'; + const isPreviewForCurrentUser = params.variant === PROFILE_PAGE_PENDING_APPROVAL_VARIANT; const fetchCurrentUserOptions = { updateHasListings: false, updateNotifications: false, diff --git a/src/containers/ProfilePage/ProfilePage.js b/src/containers/ProfilePage/ProfilePage.js index ff313f7db..a884c4f99 100644 --- a/src/containers/ProfilePage/ProfilePage.js +++ b/src/containers/ProfilePage/ProfilePage.js @@ -13,6 +13,7 @@ import { SCHEMA_TYPE_TEXT, propTypes, } from '../../util/types'; +import { PROFILE_PAGE_PENDING_APPROVAL_VARIANT } from '../../util/urlHelpers'; import { pickCustomFieldProps } from '../../util/fieldHelpers'; import { isUserAuthorized } from '../../util/userHelpers'; import { richText } from '../../util/richText'; @@ -274,9 +275,8 @@ export const ProfilePageComponent = props => { user, ...rest } = props; - // TODO pending-approval vs 'preview' const isVariant = pathParams.variant?.length > 0; - const isPreview = isVariant && pathParams.variant === 'pending-approval'; + const isPreview = isVariant && pathParams.variant === PROFILE_PAGE_PENDING_APPROVAL_VARIANT; // Stripe's onboarding needs a business URL for each seller, but the profile page can be // too empty for the provider at the time they are creating their first listing. diff --git a/src/containers/ProfilePage/ProfilePage.test.js b/src/containers/ProfilePage/ProfilePage.test.js index fbaa0405f..1974d30f6 100644 --- a/src/containers/ProfilePage/ProfilePage.test.js +++ b/src/containers/ProfilePage/ProfilePage.test.js @@ -99,6 +99,7 @@ describe('ProfilePage', () => { scrollingDisabled: false, intl: fakeIntl, viewport: fakeViewport, + params: {}, }; it('Check that user name and bio is shown correctly', () => { diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js index fea985adb..b4c1e7949 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js @@ -6,6 +6,7 @@ import { connect } from 'react-redux'; import { useConfiguration } from '../../context/configurationContext'; import { FormattedMessage, injectIntl, intlShape } from '../../util/reactIntl'; import { propTypes } from '../../util/types'; +import { PROFILE_PAGE_PENDING_APPROVAL_VARIANT } from '../../util/urlHelpers'; import { ensureCurrentUser } from '../../util/data'; import { initialValuesForUserFields, @@ -37,7 +38,7 @@ const ViewProfileLink = props => { diff --git a/src/util/urlHelpers.js b/src/util/urlHelpers.js index 12199122a..98e6ca4be 100644 --- a/src/util/urlHelpers.js +++ b/src/util/urlHelpers.js @@ -15,6 +15,8 @@ export const LISTING_PAGE_PARAM_TYPES = [ LISTING_PAGE_PARAM_TYPE_EDIT, ]; +export const PROFILE_PAGE_PENDING_APPROVAL_VARIANT = 'pending-approval'; + // No access page - path params: export const NO_ACCESS_PAGE_POST_LISTINGS = 'posting-right'; // If user account is on pending-approval state, then user can't initiate transactions or create listings From 3ddd96c081e29c4b59940f74fb501ef483b573e8 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 7 Aug 2024 16:31:49 +0300 Subject: [PATCH 18/22] ModalMissingInformation: don't show nagging modal when state is 'pending-approval' or 'banned' --- .../ModalMissingInformation/ModalMissingInformation.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ModalMissingInformation/ModalMissingInformation.js b/src/components/ModalMissingInformation/ModalMissingInformation.js index a40723e1b..9a4182f3d 100644 --- a/src/components/ModalMissingInformation/ModalMissingInformation.js +++ b/src/components/ModalMissingInformation/ModalMissingInformation.js @@ -7,6 +7,7 @@ import { useRouteConfiguration } from '../../context/routeConfigurationContext'; import { FormattedMessage } from '../../util/reactIntl'; import { ensureCurrentUser } from '../../util/data'; import { propTypes } from '../../util/types'; +import { isUserAuthorized } from '../../util/userHelpers'; import { pathByRouteName } from '../../util/routes'; import { Modal } from '../../components'; @@ -104,7 +105,7 @@ class ModalMissingInformation extends Component { let content = null; const currentUserLoaded = user && user.id; - if (currentUserLoaded) { + if (currentUserLoaded && isUserAuthorized(currentUser)) { if (this.state.showMissingInformationReminder === EMAIL_VERIFICATION) { content = ( Date: Wed, 7 Aug 2024 16:42:29 +0300 Subject: [PATCH 19/22] Routes.js: do not allow banned users to auth pages --- src/routing/Routes.js | 23 +++++++++++++++++++---- src/util/types.js | 17 ++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/routing/Routes.js b/src/routing/Routes.js index a81d273bd..d4bb799d2 100644 --- a/src/routing/Routes.js +++ b/src/routing/Routes.js @@ -17,10 +17,16 @@ import NotFoundPage from '../containers/NotFoundPage/NotFoundPage'; import LoadableComponentErrorBoundary from './LoadableComponentErrorBoundary/LoadableComponentErrorBoundary'; +const isBanned = currentUser => { + const isBrowser = typeof window !== 'undefined'; + // Future todo: currentUser?.attributes?.state === 'banned' + return isBrowser && currentUser?.attributes?.banned === true; +}; + const canShowComponent = props => { - const { isAuthenticated, route } = props; + const { isAuthenticated, currentUser, route } = props; const { auth } = route; - return !auth || isAuthenticated; + return !auth || (isAuthenticated && !isBanned(currentUser)); }; const callLoadData = props => { @@ -124,12 +130,17 @@ class RouteComponentRenderer extends Component { } render() { - const { route, match, location, staticContext } = this.props; + const { route, match, location, staticContext, currentUser } = this.props; const { component: RouteComponent, authPage = 'SignupPage', extraProps } = route; const canShow = canShowComponent(this.props); if (!canShow) { staticContext.unauthorized = true; } + + const hasCurrentUser = !!currentUser?.id; + const restrictedPageWithCurrentUser = !canShow && hasCurrentUser; + // Banned users are redirected to LandingPage + const isBannedFromAuthPages = restrictedPageWithCurrentUser && isBanned(currentUser); return canShow ? ( + ) : isBannedFromAuthPages ? ( + ) : ( { const { isAuthenticated, logoutInProgress } = state.auth; - return { isAuthenticated, logoutInProgress }; + const { currentUser } = state.user; + return { isAuthenticated, logoutInProgress, currentUser }; }; const RouteComponentContainer = compose(connect(mapStateToProps))(RouteComponentRenderer); diff --git a/src/util/types.js b/src/util/types.js index 3164b0b0a..6359905dc 100644 --- a/src/util/types.js +++ b/src/util/types.js @@ -125,7 +125,7 @@ propTypes.imageAsset = shape({ }); // Denormalised user object -propTypes.currentUser = shape({ +const currentUser = shape({ id: propTypes.uuid.isRequired, type: propTypes.value('currentUser').isRequired, attributes: shape({ @@ -143,6 +143,21 @@ propTypes.currentUser = shape({ }), profileImage: propTypes.image, }); +const currentUserBanned = shape({ + id: propTypes.uuid.isRequired, + type: propTypes.value('currentUser').isRequired, + attributes: shape({ + banned: propTypes.value(true).isRequired, + }), +}); +const currentUserDeleted = shape({ + id: propTypes.uuid.isRequired, + type: propTypes.value('currentUser').isRequired, + attributes: shape({ + deleted: propTypes.value(true).isRequired, + }), +}); +propTypes.currentUser = oneOfType([currentUser, currentUserBanned, currentUserDeleted]); const userAttributes = shape({ banned: propTypes.value(false).isRequired, From 72b3648d49861983b42940f91dbf6dc141925e5d Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 7 Aug 2024 16:43:18 +0300 Subject: [PATCH 20/22] InboxPage.duck.js: return deleted and banned flags. --- src/containers/InboxPage/InboxPage.duck.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/InboxPage/InboxPage.duck.js b/src/containers/InboxPage/InboxPage.duck.js index 1709f5a72..b00099bc6 100644 --- a/src/containers/InboxPage/InboxPage.duck.js +++ b/src/containers/InboxPage/InboxPage.duck.js @@ -111,7 +111,7 @@ export const loadData = (params, search) => (dispatch, getState, sdk) => { 'lineItems', ], 'fields.listing': ['title', 'availabilityPlan', 'publicData.listingType'], - 'fields.user': ['profile.displayName', 'profile.abbreviatedName'], + 'fields.user': ['profile.displayName', 'profile.abbreviatedName', 'deleted', 'banned'], 'fields.image': ['variants.square-small', 'variants.square-small2x'], page, perPage: INBOX_PAGE_SIZE, From acab151eb4f1451e65c7325c8c39ce5903b1e645 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Wed, 14 Aug 2024 15:47:55 +0300 Subject: [PATCH 21/22] ProfilePage: when data is not yet loaded, render nothing --- src/containers/ProfilePage/ProfilePage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/containers/ProfilePage/ProfilePage.js b/src/containers/ProfilePage/ProfilePage.js index a884c4f99..56814a49f 100644 --- a/src/containers/ProfilePage/ProfilePage.js +++ b/src/containers/ProfilePage/ProfilePage.js @@ -290,6 +290,9 @@ export const ProfilePageComponent = props => { return ; } + const isDataLoaded = isPreview + ? currentUser != null || userShowError != null + : user != null || userShowError != null; const isCurrentUser = currentUser?.id && currentUser?.id?.uuid === pathParams.id; const profileUser = useCurrentUser ? currentUser : user; const { bio, displayName, publicData, metadata } = profileUser?.attributes?.profile || {}; @@ -298,7 +301,9 @@ export const ProfilePageComponent = props => { const schemaTitleVars = { name: displayName, marketplaceName: config.marketplaceName }; const schemaTitle = intl.formatMessage({ id: 'ProfilePage.schemaTitle' }, schemaTitleVars); - if (!isPreview && userShowError && userShowError.status === 404) { + if (!isDataLoaded) { + return null; + } else if (!isPreview && userShowError && userShowError.status === 404) { return ; } else if (isPreview && mounted && !isCurrentUser) { // Someone is manipulating the URL, redirect to current user's profile page. From 64fe3a3b89ac0ab9e95dcff6118df1f8e7ff42d4 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Fri, 16 Aug 2024 12:58:00 +0300 Subject: [PATCH 22/22] Update Changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c7304fe..d679ecf84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,20 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2024-XX-XX +- [add] Access control: 'pending-approval' state for users. + + - Users will get "state", which is exposed through currentUser's attribute + - A new state is "pending-approval", which restricts user from initiating transactions and posting + listings. + - In addition, 'banned' users will also have state 'banned'. + - Extra: Routes.js: do not allow banned users to auth pages + - [fix]: InboxPage.duck.js: include deleted and banned attributes + - [fix]: ModalMissingInformation: only 'active' users get this modal shown + - [fix]: Inquiry modal: open the modal after authentication + - Some util-file imports have been reordered (might cause conflicts) + + [#428](https://github.com/sharetribe/web-template/pull/428) + - [fix] SearchPage: SearchFiltersMobile (modal) should be above topbar. [#432](https://github.com/sharetribe/web-template/pull/432)