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

chore: Misc enhancements to bank details #11053

Merged
merged 1 commit into from
Feb 25, 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
9 changes: 6 additions & 3 deletions components/contribution-flow/ContributionFlowSuccess.js
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,12 @@ class ContributionFlowSuccess extends React.Component {
const platformTipAmount = get(this.props.data, 'order.platformTipAmount.valueInCents', 0);
const totalAmount = amount + platformTipAmount;
const currency = get(this.props.data, 'order.amount.currency');
const formattedAmount = formatCurrency(totalAmount, currency, { locale: this.props.intl.locale });
const formattedAmount = formatCurrency(totalAmount, currency, {
locale: this.props.intl.locale,
currencyDisplay: 'code',
});

const formatValues = {
const formattedValues = {
account: bankAccount ? formatAccountDetails(bankAccount) : '',
reference: get(this.props.data, 'order.legacyId', null),
amount: formattedAmount,
Expand All @@ -342,7 +345,7 @@ class ContributionFlowSuccess extends React.Component {
<FormattedMessage id="NewContributionFlow.PaymentInstructions" defaultMessage="Payment instructions" />
</H3>
<Flex mt={2}>
<Flex style={{ whiteSpace: 'pre-wrap' }}>{formatManualInstructions(instructions, formatValues)}</Flex>
<Flex style={{ whiteSpace: 'pre-wrap' }}>{formatManualInstructions(instructions, formattedValues)}</Flex>
</Flex>
</BankTransferInfoContainer>
)}
Expand Down
3 changes: 1 addition & 2 deletions components/contribution-flow/query-parameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,7 @@ const EmbedContributionFlowUrlParametersConfig = {
*/
useTheme: { type: 'boolean', static: true },
/**
* Whether to redirect the parent of the iframe rather than the iframe itself. The `iframe` needs to have
* its `sandbox` property set to `allow-top-navigation` for this to work.
* Whether to redirect the parent of the iframe rather than the iframe itself. The `iframe` needs to have its `sandbox` property set to `allow-top-navigation` for this to work.
*/
shouldRedirectParent: { type: 'boolean', static: true },
};
Expand Down
206 changes: 125 additions & 81 deletions components/edit-collective/UpdateBankDetailsForm.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import styled from 'styled-components';
import { v4 as uuid } from 'uuid';

import { formatManualInstructions } from '../../lib/payment-method-utils';

import Container from '../Container';
import { Box, Flex } from '../Grid';
import StyledTextarea from '../StyledTextarea';
import { P, Span } from '../Text';
import { Badge } from '../ui/Badge';

import { formatAccountDetails } from './utils';

const List = styled.ul`
margin: 0;
padding: 0;
position: relative;
list-style: none;
`;
/**
* Validates the provided instructions to see if the variable usage is correct.
* Detects issues like unclosed brackets, missing variables, unknown variables, etc.
*
* @returns {Array<{key: string; message: React.ReactNode;}>} A list of errors found in the instructions.
*/
const validateInstructions = instructions => {
const errors = [];
const usedVariables = instructions.match(/{[^}]+}/g) || [];
const allowedVariables = ['account', 'amount', 'collective', 'reference'];
const requiredVariables = ['account', 'reference'];
const addError = message => errors.push({ key: uuid(), message });

usedVariables.forEach(variable => {
const varName = variable.replace(/[{}]/g, '');
if (!allowedVariables.includes(varName)) {
addError(<FormattedMessage defaultMessage="Unknown variable: {variable}" id="ZTL623" values={{ variable }} />);
}
});

requiredVariables.forEach(variable => {
if (!usedVariables.includes(`{${variable}}`)) {
addError(
<FormattedMessage defaultMessage="Missing required variable: {variable}" id="CFeTrT" values={{ variable }} />,
);
}
});

const openBrackets = (instructions.match(/{/g) || []).length;
const closeBrackets = (instructions.match(/}/g) || []).length;
if (openBrackets !== closeBrackets) {
addError(<FormattedMessage defaultMessage="Unclosed brackets" id="6LYnXr" />);
}

return errors;
};

class UpdateBankDetailsForm extends React.Component {
static propTypes = {
intl: PropTypes.object.isRequired,
profileType: PropTypes.string, // USER or ORGANIZATION
error: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
useStructuredForm: PropTypes.bool,
Expand All @@ -32,8 +60,8 @@ class UpdateBankDetailsForm extends React.Component {

constructor(props) {
super(props);
this.state = { form: { instructions: props.value } };
this.onChange = this.onChange.bind(this);
this.state = { instructions: props.value || '', errors: [] };
this.onInstructionsChange = this.onInstructionsChange.bind(this);
this.messages = defineMessages({
'bankaccount.instructions.label': {
id: 'paymentMethods.manual.instructions',
Expand All @@ -42,101 +70,117 @@ class UpdateBankDetailsForm extends React.Component {
});
}

onChange(field, value) {
onInstructionsChange(e) {
const value = e.target.value;
this.setState(
state => {
return { form: { ...state.form, [field]: value } };
() => {
return { instructions: value, errors: validateInstructions(value) };
},
() => {
return this.props.onChange(this.state.form);
return this.props.onChange({ instructions: value });
},
);
}

renderInstructions() {
const formatValues = {
const formattedValues = {
account: this.props.bankAccount ? formatAccountDetails(this.props.bankAccount) : '',
reference: '76400',
OrderId: '76400',
amount: '$30',
amount: '30,00 USD',
collective: 'acme',
};

return formatManualInstructions(this.state.form.instructions, formatValues);
return formatManualInstructions(this.state.instructions, formattedValues);
}

render() {
const { intl, value, error, useStructuredForm, bankAccount } = this.props;
const { intl, value, useStructuredForm, bankAccount } = this.props;
return (
<Flex flexDirection="column">
<Container as="fieldset" border="none" width={1}>
<Flex flexDirection={['column-reverse', 'row']}>
<Box mb={3} flexGrow={1}>
<div>
<div className="mb-3 text-sm">
<p>
<FormattedMessage
id="bankaccount.instructions.variables"
defaultMessage="Financial contributors will receive these instructions when they select bank transfer as the payment method. You can use the following variables (like blanks that gets filled in):"
/>
</p>

<ul className="list list-inside list-disc space-y-1">
{useStructuredForm && bankAccount?.currency && (
<li>
<Badge size="sm">&#123;account&#125;</Badge>:{' '}
<FormattedMessage
id="bankaccount.instructions.account"
defaultMessage="The bank account details you added above."
/>
</li>
)}
<li>
<Badge size="sm">&#123;amount&#125;</Badge>:{' '}
<FormattedMessage
id="bankaccount.instructions.amount"
defaultMessage="Total amount the payer should transfer."
/>
</li>
<li>
<Badge size="sm">&#123;collective&#125;</Badge>:{' '}
<FormattedMessage
id="bankaccount.instructions.collective"
defaultMessage="Collective to receive the funds. If you only have one Collective, you might not need to include this."
/>
</li>
<li>
<Badge size="sm">&#123;reference&#125;</Badge>:{' '}
<FormattedMessage
id="bankaccount.instructions.reference"
defaultMessage="Unique ID code, to confirm receipt of funds."
/>
</li>
</ul>
</div>

<div>
<div className="flex flex-col sm:flex-row">
<div className="flex-1">
<label className="mb-1 block font-bold" htmlFor="bank-account-instructions">
<FormattedMessage defaultMessage="Template" id="3JxaKs" />
</label>
<StyledTextarea
label={intl.formatMessage(this.messages['bankaccount.instructions.label'])}
htmlFor="instructions"
id="bank-account-instructions"
width="100%"
height={400}
onChange={e => this.onChange('instructions', e.target.value)}
minHeight={550}
maxHeight={550}
onChange={this.onInstructionsChange}
defaultValue={value}
/>
</Box>
<Container fontSize="0.85rem" pl={[0, 3]} width={[1, 0.5]}>
<P>
<FormattedMessage
id="bankaccount.instructions.variables"
defaultMessage="Financial contributors will receive these instructions when they select bank transfer as the payment method. You can use the following variables (like blanks that gets filled in):"
/>
</P>

<List>
{useStructuredForm && bankAccount?.currency && (
<li>
<code>&#123;account&#125;</code>:{' '}
<FormattedMessage
id="bankaccount.instructions.account"
defaultMessage="The bank account details you added above."
/>
</li>
)}
<li>
<code>&#123;amount&#125;</code>:{' '}
<FormattedMessage
id="bankaccount.instructions.amount"
defaultMessage="Total amount the payer should transfer."
/>
</li>
<li>
<code>&#123;collective&#125;</code>:{' '}
<FormattedMessage
id="bankaccount.instructions.collective"
defaultMessage="Collective to receive the funds. If you only have one Collective, you might not need to include this."
/>
</li>
<li>
<code>&#123;reference&#125;</code>:{' '}
<FormattedMessage
id="bankaccount.instructions.reference"
defaultMessage="Unique ID code, to confirm receipt of funds."
/>
</li>

<P>
<FormattedMessage id="bankaccount.instructions.preview" defaultMessage="Preview:" />
</P>

<pre style={{ whiteSpace: 'pre-wrap' }}>{this.renderInstructions()}</pre>
</List>
</div>
<Container pl={[0, 3]} width={[1, 0.5]}>
<p className="mb-1 font-bold">
<FormattedMessage defaultMessage="Preview" id="TJo5E6" />
</p>

<pre className="h-[550px] overflow-y-auto rounded border bg-neutral-100 px-4 py-3 text-sm whitespace-pre-wrap">
{this.renderInstructions()}
</pre>
</Container>
</Flex>
</Container>
</div>
</div>

{error && (
<Span display="block" color="red.500" pt={2} fontSize="10px">
{error}
</Span>
{this.state.errors?.length > 0 && (
<div className="mt-3 rounded-sm border-l-4 border-yellow-500 bg-yellow-100 p-3 text-sm">
<p className="mb-1 font-bold">
<FormattedMessage defaultMessage="Warning" id="3SVI5p" />
</p>
<ul className="list list-inside list-disc">
{this.state.errors.map(({ key, message }) => (
<li key={key}>{message}</li>
))}
</ul>
</div>
)}
</Flex>
</div>
);
}
}
Expand Down
Loading
Loading