Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ContributorFlow: Fetch ContributorProfiles from the API #10911

Merged
merged 6 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion components/SignInOrJoinFree.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ class SignInOrJoinFree extends React.Component {
// In dev/test, API directly returns a redirect URL for emails like
// test*@opencollective.com.
if (response.redirect) {
await this.props.router.replace(response.redirect);
// Use browser redirection to guarantee page, login, and router state are all updated.
window.location.href = response.redirect;
} else if (response.token) {
const user = await this.props.login(response.token);
if (!user) {
Expand Down
24 changes: 15 additions & 9 deletions components/contribution-flow/ContributeProfilePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Span } from '../Text';

import { canUseIncognitoForContribution } from './utils';

const { USER, ORGANIZATION, COLLECTIVE, FUND, EVENT, PROJECT } = CollectiveType;
const { ORGANIZATION, COLLECTIVE, FUND, EVENT, PROJECT, INDIVIDUAL } = CollectiveType;

const formatAccountName = (intl, account) => {
return account.isIncognito
Expand All @@ -21,22 +21,28 @@ const formatAccountName = (intl, account) => {
};

const getProfileOptions = (intl, profiles, tier) => {
const getOptionFromAccount = value => ({ [FLAG_COLLECTIVE_PICKER_COLLECTIVE]: true, value, label: value.name });
const getOptionFromAccount = value => ({
[FLAG_COLLECTIVE_PICKER_COLLECTIVE]: true,
value: value.account,
label: value.account.name,
});
const sortOptions = options => sortBy(options, 'value.name');
const profileOptions = profiles.map(getOptionFromAccount);
const profilesByType = groupBy(profileOptions, p => p.value.type);
const myself = profilesByType[USER] || [];
const myself = profilesByType[INDIVIDUAL] || [];
const myOrganizations = sortOptions(profilesByType[ORGANIZATION] || []);

// Add incognito profile entry if it doesn't exists
const hasIncognitoProfile = profiles.some(p => p.type === CollectiveType.USER && p.isIncognito);
const hasIncognitoProfile = profiles.some(p => p.account.type === CollectiveType.INDIVIDUAL && p.account.isIncognito);
if (!hasIncognitoProfile && canUseIncognitoForContribution(tier)) {
myself.push(
getOptionFromAccount({
id: 'incognito',
type: CollectiveType.USER,
isIncognito: true,
name: intl.formatMessage({ id: 'profile.incognito', defaultMessage: 'Incognito' }),
account: {
id: 'incognito',
type: CollectiveType.INDIVIDUAL,
isIncognito: true,
name: intl.formatMessage({ id: 'profile.incognito', defaultMessage: 'Incognito' }),
},
}),
);
}
Expand Down Expand Up @@ -99,7 +105,7 @@ const formatProfileOption = (option, _, intl) => {
</Span>
) : (
<Span fontSize="12px" lineHeight="18px" color="black.700">
{account.type === 'USER' && (
{account.type === CollectiveType.INDIVIDUAL && (
<React.Fragment>
<FormattedMessage id="ContributionFlow.PersonalProfile" defaultMessage="Personal profile" />
{' - '}
Expand Down
4 changes: 2 additions & 2 deletions components/contribution-flow/ContributionFlowStepContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class ContributionFlowStepContainer extends React.Component {
intl: PropTypes.object,
LoggedInUser: PropTypes.object,
collective: PropTypes.object,
contributorProfiles: PropTypes.arrayOf(PropTypes.object),
tier: PropTypes.object,
onChange: PropTypes.func,
showPlatformTip: PropTypes.bool,
Expand All @@ -33,7 +34,6 @@ class ContributionFlowStepContainer extends React.Component {
step: PropTypes.shape({
name: PropTypes.string,
}),
contributeProfiles: PropTypes.arrayOf(PropTypes.object),
mainState: PropTypes.shape({
stepDetails: PropTypes.object,
stepProfile: PropTypes.shape({
Expand Down Expand Up @@ -92,7 +92,7 @@ class ContributionFlowStepContainer extends React.Component {
case 'profile': {
return (
<StepProfile
profiles={this.props.contributeProfiles}
profiles={this.props.contributorProfiles}
collective={collective}
tier={tier}
stepDetails={stepDetails}
Expand Down
10 changes: 5 additions & 5 deletions components/contribution-flow/StepProfileGuestForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,6 @@ const StepProfileGuestForm = ({ stepDetails, onChange, data, isEmbed, onSignInCl
/>
)}
</StyledInputField>
{isCaptchaEnabled() && (
<Flex mt="18px" justifyContent="center">
<Captcha onVerify={result => dispatchChange('captcha', result)} />
</Flex>
)}
{contributionRequiresAddress(stepDetails, tier) && (
<React.Fragment>
<Flex alignItems="center" my="14px">
Expand All @@ -166,6 +161,11 @@ const StepProfileGuestForm = ({ stepDetails, onChange, data, isEmbed, onSignInCl
/>
</React.Fragment>
)}
{isCaptchaEnabled() && (
<Flex mt="18px" justifyContent="center">
<Captcha onVerify={result => dispatchChange('captcha', result)} />
</Flex>
)}
<StepProfileInfoMessage isGuest hasLegalNameField />
<P color="black.500" fontSize="12px" mt={4} data-cy="join-conditions">
<FormattedMessage
Expand Down
30 changes: 30 additions & 0 deletions components/contribution-flow/graphql/queries.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,36 @@ export const contributionFlowAccountQuery = gql`
id
...ContributionFlowAccountFields
}
me {
contributorProfiles(forAccount: { slug: $collectiveSlug }) {
account {
id
name
legalName
slug
type
imageUrl(height: 192)
isIncognito
... on Individual {
email
isGuest
}
location {
address
country
structured
}
... on AccountWithHost {
host {
id
slug
name
imageUrl(height: 64)
}
}
}
}
}
tier(tier: { legacyId: $tierId }, throwIfMissing: false) @include(if: $includeTier) {
id
legacyId
Expand Down
31 changes: 17 additions & 14 deletions components/contribution-flow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { formatErrorMessage, getErrorFromGraphqlException } from '../../lib/erro
import { isPastEvent } from '../../lib/events';
import { Experiment, isExperimentEnabled } from '../../lib/experiments/experiments';
import { API_V2_CONTEXT, gql } from '../../lib/graphql/helpers';
import { AccountType } from '../../lib/graphql/types/v2/schema';
import { addCreateCollectiveMutation } from '../../lib/graphql/v1/mutations';
import { setGuestToken } from '../../lib/guest-accounts';
import { getStripe, stripeTokenToPaymentMethod } from '../../lib/stripe';
Expand Down Expand Up @@ -64,7 +65,6 @@ import SignInToContributeAsAnOrganization from './SignInToContributeAsAnOrganiza
import { validateGuestProfile } from './StepProfileGuestForm';
import { NEW_ORGANIZATION_KEY } from './StepProfileLoggedInForm';
import {
getContributeProfiles,
getGQLV2AmountInput,
getGuestInfoFromStepProfile,
getTotalAmount,
Expand Down Expand Up @@ -136,6 +136,7 @@ class ContributionFlow extends React.Component {
slug: PropTypes.string,
}),
}).isRequired,
contributorProfiles: PropTypes.arrayOf(PropTypes.object),
host: PropTypes.object.isRequired,
tier: PropTypes.object,
intl: PropTypes.object,
Expand Down Expand Up @@ -222,7 +223,10 @@ class ContributionFlow extends React.Component {
// User has logged out, reset the state
this.setState({ stepProfile: null, stepSummary: null, stepPayment: null });
this.pushStepRoute(STEPS.PROFILE);
} else if (!oldProps.LoggedInUser && this.props.LoggedInUser) {
} else if (
(!oldProps.LoggedInUser && this.props.LoggedInUser) ||
(oldProps.contributorProfiles.length === 0 && this.props.contributorProfiles.length > 0)
) {
// User has logged in, reload the step profile
this.setState({ stepProfile: this.getDefaultStepProfile() });

Expand Down Expand Up @@ -391,8 +395,8 @@ class ContributionFlow extends React.Component {
await confirmPayment(stripeData?.stripe, stripeData?.paymentIntentClientSecret, {
returnUrl: returnUrl.href,
elements: stripeData?.elements,
type: stepPayment?.paymentMethod?.type,
paymentMethodId: stepPayment?.paymentMethod?.data?.stripePaymentMethodId,
type: stepPayment.paymentMethod?.type,
paymentMethodId: stepPayment.paymentMethod?.data?.stripePaymentMethodId,
});
this.setState({ isSubmitted: true, isSubmitting: false });
return this.handleSuccess(order);
Expand Down Expand Up @@ -463,12 +467,9 @@ class ContributionFlow extends React.Component {
};

// ---- Getters ----

getContributeProfiles = memoizeOne(getContributeProfiles);

getDefaultStepProfile() {
const { LoggedInUser, loadingLoggedInUser, collective, tier } = this.props;
const profiles = this.getContributeProfiles(LoggedInUser, collective, tier);
const { loadingLoggedInUser, contributorProfiles } = this.props;
const profiles = contributorProfiles || [];
const queryParams = this.getQueryParams();

// We want to wait for the user to be logged in before matching the profile
Expand All @@ -480,17 +481,19 @@ class ContributionFlow extends React.Component {
let contributorProfile;
if (queryParams.contributeAs && queryParams.contributeAs !== PERSONAL_PROFILE_ALIAS) {
if (queryParams.contributeAs === INCOGNITO_PROFILE_ALIAS) {
contributorProfile = profiles.find(({ isIncognito }) => isIncognito);
contributorProfile = profiles.find(({ account: { isIncognito } }) => isIncognito);
} else if (queryParams.contributeAs === 'me') {
contributorProfile = profiles.find(({ account: { type } }) => type === AccountType.INDIVIDUAL);
} else {
contributorProfile = profiles.find(({ slug }) => slug === queryParams.contributeAs);
contributorProfile = profiles.find(({ account: { slug } }) => slug === queryParams.contributeAs);
}
}

if (contributorProfile) {
return contributorProfile;
} else if (profiles[0]) {
} else if (profiles[0]?.account) {
// Otherwise to the logged-in user personal profile, if any
return profiles[0];
return profiles[0].account;
}

// Otherwise, it's a guest contribution
Expand Down Expand Up @@ -1012,7 +1015,7 @@ class ContributionFlow extends React.Component {
isSubmitting={isValidating || isLoading}
disabledPaymentMethodTypes={queryParams.disabledPaymentMethodTypes}
hideCreditCardPostalCode={queryParams.hideCreditCardPostalCode}
contributeProfiles={this.getContributeProfiles(LoggedInUser, collective, tier)}
contributorProfiles={this.props.contributorProfiles}
/>
<Box mt={40}>
<ContributionFlowButtons
Expand Down
52 changes: 4 additions & 48 deletions components/contribution-flow/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { CreditCard } from '@styled-icons/fa-solid/CreditCard';
import { find, get, isEmpty, pick, sortBy, uniqBy } from 'lodash';
import { find, get, pick, sortBy, uniqBy } from 'lodash';
import { defineMessages, FormattedMessage } from 'react-intl';

import { canContributeRecurring, getCollectivePageMetadata } from '../../lib/collective';
Expand All @@ -11,7 +11,6 @@ import {
PAYMENT_METHOD_SERVICE,
PAYMENT_METHOD_TYPE,
} from '../../lib/constants/payment-methods';
import roles from '../../lib/constants/roles';
import { TierTypes } from '../../lib/constants/tiers-types';
import { PaymentMethodService, PaymentMethodType } from '../../lib/graphql/types/v2/schema';
import { getPaymentMethodName } from '../../lib/payment_method_label';
Expand All @@ -29,56 +28,13 @@ export const NEW_CREDIT_CARD_KEY = 'newCreditCard';
export const STRIPE_PAYMENT_ELEMENT_KEY = 'stripe-payment-element';
const PAYPAL_MAX_AMOUNT = 999999999; // See MAX_VALUE_EXCEEDED https://developer.paypal.com/api/rest/reference/orders/v2/errors/#link-createorder

const memberCanBeUsedToContribute = (member, account, canUseIncognito) => {
if (member.role !== roles.ADMIN) {
return false;
} else if (!canUseIncognito && member.collective.isIncognito) {
// Incognito can't be used to contribute if not allowed
return false;
} else if (
[CollectiveType.COLLECTIVE, CollectiveType.FUND].includes(member.collective.type) &&
member.collective.host?.id !== account.host.legacyId
) {
// If the contributing account is fiscally hosted, the host must be the same as the one you're contributing to
return false;
} else {
return true;
}
};

/*
**Cannot use contributions for events and "Tickets" tiers, because we need the ticket holder's identity
*/
export const canUseIncognitoForContribution = tier => {
return !tier || tier.type !== 'TICKET';
};

export const getContributeProfiles = (loggedInUser, collective, tier) => {
if (!loggedInUser) {
return [];
} else {
const canUseIncognito = canUseIncognitoForContribution(tier);
const filteredMembers = loggedInUser.memberOf.filter(member =>
memberCanBeUsedToContribute(member, collective, canUseIncognito),
);
const personalProfile = { email: loggedInUser.email, image: loggedInUser.image, ...loggedInUser.collective };
const contributorProfiles = [personalProfile];
filteredMembers.forEach(member => {
// Account can't contribute to itself
if (member.collective.id !== collective.legacyId) {
contributorProfiles.push(member.collective);
}
if (!isEmpty(member.collective.children)) {
const childrenOfSameHost = member.collective.children.filter(
child => child.host && child.host.id === collective.host.legacyId,
);
contributorProfiles.push(...childrenOfSameHost);
}
});
return uniqBy([personalProfile, ...contributorProfiles], 'id');
}
};

export const generatePaymentMethodOptions = (
intl,
paymentMethods,
Expand Down Expand Up @@ -111,7 +67,7 @@ export const generatePaymentMethodOptions = (

uniquePMs = uniquePMs.filter(
({ paymentMethod }) =>
paymentMethod.type !== PAYMENT_METHOD_TYPE.COLLECTIVE || collective.host.legacyId === stepProfile.host?.id,
paymentMethod.type !== PAYMENT_METHOD_TYPE.COLLECTIVE || collective.host.id === stepProfile.host?.id,
);

if (paymentIntent) {
Expand All @@ -127,7 +83,7 @@ export const generatePaymentMethodOptions = (

return (
allowedStripeTypes.includes(paymentMethod.type.toLowerCase()) &&
(!paymentMethod?.data?.stripeAccount || paymentMethod?.data?.stripeAccount === paymentIntent.stripeAccount)
(!paymentMethod.data?.stripeAccount || paymentMethod.data?.stripeAccount === paymentIntent.stripeAccount)
);
});
} else {
Expand All @@ -136,7 +92,7 @@ export const generatePaymentMethodOptions = (
return true;
}

return paymentMethod.type === PaymentMethodType.CREDITCARD && !paymentMethod?.data?.stripeAccount;
return paymentMethod.type === PaymentMethodType.CREDITCARD && !paymentMethod.data?.stripeAccount;
});
}

Expand Down
4 changes: 2 additions & 2 deletions lib/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3572,7 +3572,7 @@ type TaxInfo {
"""
Identifier for this tax (GST, VAT, etc)
"""
type: OrderTaxType!
type: TaxType!

"""
Percentage applied, between 0-100
Expand All @@ -3593,7 +3593,7 @@ type TaxInfo {
"""
The type of a tax like GST, VAT, etc
"""
enum OrderTaxType {
enum TaxType {
"""
European Value Added Tax
"""
Expand Down
Loading
Loading