Skip to content

Commit

Permalink
Merge pull request #428 from sharetribe/user-pending-approval
Browse files Browse the repository at this point in the history
User state: pending approval
  • Loading branch information
Gnito authored Aug 19, 2024
2 parents 4f6c242 + 64fe3a3 commit 6bb7a84
Show file tree
Hide file tree
Showing 49 changed files with 424 additions and 114 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
18 changes: 15 additions & 3 deletions src/components/Avatar/Avatar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React from 'react';

// Import config and utils
import { useIntl } from '../../util/reactIntl';
import {
SCHEMA_TYPE_ENUM,
SCHEMA_TYPE_MULTI_ENUM,
SCHEMA_TYPE_TEXT,
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';

Expand Down
2 changes: 1 addition & 1 deletion src/components/FieldCurrencyInput/FieldCurrencyInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
4 changes: 3 additions & 1 deletion src/components/LimitedAccessBanner/LimitedAccessBanner.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = (
<EmailReminder
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/components/OrderPanel/OrderPanel.example.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
7 changes: 4 additions & 3 deletions src/components/PaginationLinks/PaginationLinks.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
3 changes: 2 additions & 1 deletion src/components/ReviewRating/ReviewRating.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
6 changes: 4 additions & 2 deletions src/components/Reviews/Reviews.js
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const props = {
isAuthenticated: false,
authInProgress: false,
scrollingDisabled: false,
currentUserHasListings: false,
onLogout: noop,
onManageDisableScrolling: noop,
onResendVerificationEmail: noop,
Expand Down
7 changes: 6 additions & 1 deletion src/containers/CheckoutPage/CheckoutPage.duck.js
Original file line number Diff line number Diff line change
Expand Up @@ -478,8 +478,13 @@ 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'] },
updateHasListings: false,
updateNotifications: false,
};

return dispatch(fetchCurrentUser({ include: ['stripeCustomer.defaultPaymentMethod'] }))
return dispatch(fetchCurrentUser(fetchCurrentUserOptions))
.then(response => {
dispatch(stripeCustomerSuccess());
})
Expand Down
32 changes: 23 additions & 9 deletions src/containers/CheckoutPage/CheckoutPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -64,6 +66,7 @@ const EnhancedCheckoutPage = props => {

useEffect(() => {
const {
currentUser,
orderData,
listing,
transaction,
Expand All @@ -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,
});
}
}
}, []);

Expand All @@ -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)
Expand All @@ -111,6 +118,13 @@ const EnhancedCheckoutPage = props => {
listing,
});
return <NamedRedirect name="ListingPage" params={params} />;
} else if (shouldRedirectUnathorizedUser) {
return (
<NamedRedirect
name="NoAccessPage"
params={{ missingAccessRight: NO_ACCESS_PAGE_USER_PENDING_APPROVAL }}
/>
);
}

const listingTitle = listing?.attributes?.title;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ describe('ContactDetailsPageComponent', () => {
scrollingDisabled={false}
authInProgress={false}
currentUser={createCurrentUser('user1')}
currentUserHasListings={false}
isAuthenticated={false}
onChange={noop}
onLogout={noop}
Expand Down
35 changes: 23 additions & 12 deletions src/containers/EditListingPage/EditListingPage.duck.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -916,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) {
Expand All @@ -933,20 +937,27 @@ 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;
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;
Expand Down
13 changes: 10 additions & 3 deletions src/containers/EditListingPage/EditListingPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ 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';

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 {
Expand Down Expand Up @@ -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 (
<NamedRedirect
name="NoAccessPage"
params={{ missingAccessRight: NO_ACCESS_PAGE_USER_PENDING_APPROVAL }}
/>
);
} else if (shouldRedirectNoPostingRights) {
return (
<NamedRedirect
name="NoAccessPage"
Expand Down Expand Up @@ -289,7 +297,6 @@ EditListingPageComponent.defaultProps = {
stripeAccountFetched: null,
currentUser: null,
stripeAccount: null,
currentUserHasOrders: null,
listing: null,
listingDraft: null,
notificationCount: 0,
Expand Down
1 change: 0 additions & 1 deletion src/containers/EditListingPage/EditListingPage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2352,7 +2352,6 @@ describe('EditListingPageComponent', () => {
render(
<EditListingPageComponent
params={{ id: 'id', slug: 'slug', type: 'new', tab: 'details' }}
currentUserHasListings={false}
isAuthenticated={false}
authInProgress={false}
fetchInProgress={false}
Expand Down
Loading

0 comments on commit 6bb7a84

Please sign in to comment.