Skip to content

Commit

Permalink
feat: enable gift card boosts
Browse files Browse the repository at this point in the history
  • Loading branch information
msalcala11 committed Oct 8, 2024
1 parent 49afbe0 commit 6b2430f
Show file tree
Hide file tree
Showing 17 changed files with 237 additions and 72 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p align="center">
<img src="https://bitpay.com/assets/extension-banner.png" />
<img src="https://raw.githubusercontent.com/bitpay/bitpay-browser-extension/c5ab46d91b6b26aa2c1e748aba44418d98524030/extension-banner.png" />
</p>
<h1 align="center">
Discover new ways to use crypto
Expand Down
Binary file added extension-banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pay-with-bitpay",
"version": "1.8.4",
"version": "1.9.0",
"description": "Be alerted whenever a website you visit accepts BitPay as a payment option.",
"repository": "https://github.com/bitpay/pay-with-bitpay.git",
"author": "BitPay",
Expand Down
7 changes: 4 additions & 3 deletions source/popup/components/discount-text/discount-text.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import { Merchant, formatDiscount } from '../../../services/merchant';
import { Merchant, formatDiscount, getCouponColor } from '../../../services/merchant';
import { DirectoryDiscount } from '../../../services/directory';
import { getVisibleCoupon } from '../../../services/gift-card';

const DiscountText: React.FC<{ merchant: Merchant }> = ({ merchant }) => {
const cardConfig = merchant.giftCards[0];
const discount = merchant.discount || (cardConfig && cardConfig.discounts && cardConfig.discounts[0]);
const discount = merchant.discount || getVisibleCoupon(cardConfig);
const discountCurrency = merchant.discount ? merchant.discount.currency : cardConfig && cardConfig.currency;
const color = merchant.theme === '#ffffff' ? '#4f6ef7' : merchant.theme;
const color = getCouponColor(merchant);
const text = { color, fontWeight: 700 };
return (
<div className="ellipsis" style={text}>
Expand Down
56 changes: 41 additions & 15 deletions source/popup/components/line-items/line-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { launchNewTab } from '../../../services/browser';
import { formatDiscount } from '../../../services/merchant';
import { CardConfig, GiftCard, UnsoldGiftCard } from '../../../services/gift-card.types';
import { formatCurrency } from '../../../services/currency';
import { getTotalDiscount, getDiscountAmount, getActivationFee } from '../../../services/gift-card';
import { getTotalDiscount, getDiscountAmount, getActivationFee, hasVisibleBoost } from '../../../services/gift-card';
import './line-items.scss';

const LineItems: React.FC<{ cardConfig: CardConfig; card: Partial<GiftCard> & UnsoldGiftCard }> = ({
Expand All @@ -16,7 +16,11 @@ const LineItems: React.FC<{ cardConfig: CardConfig; card: Partial<GiftCard> & Un
}) => {
const tracking = useTracking();
const activationFee = getActivationFee(card.amount, cardConfig);
const totalDiscount = getTotalDiscount(card.amount, card.discounts || cardConfig.discounts);
const totalDiscount = getTotalDiscount(card.amount, card.coupons || card.discounts || cardConfig.coupons || cardConfig.discounts);
const boosts = card.coupons && card.coupons.filter(coupon => coupon.displayType === 'boost');
const boost = boosts && boosts[0];
const discounts = card.discounts ? card.discounts : (card.coupons && card.coupons.filter(coupon => coupon.displayType === 'discount'));
const discount = discounts && discounts[0];
const openInvoice = (url: string) => (): void => {
launchNewTab(`${url}&view=popup`);
tracking.trackEvent({ action: 'clickedAmountPaid' });
Expand All @@ -29,6 +33,29 @@ const LineItems: React.FC<{ cardConfig: CardConfig; card: Partial<GiftCard> & Un
<div className="line-items__item__value">{format(new Date(card.date), 'MMM dd yyyy')}</div>
</div>
)}
{hasVisibleBoost(cardConfig) && (
<div className="line-items__item line-items__item">
<div className="line-items__item__label line-items__item__label">
Entered Amount
</div>
<div className="line-items__item__value line-items__item__value">
{formatCurrency(card.amount - totalDiscount, card.currency, { hideSymbol: true })}
</div>
</div>
)}
{boost && (
<div className="line-items__item">
<div className="line-items__item__label">
{boost.code ? `${formatDiscount(boost, cardConfig.currency, true)} ` : ''}Boost
</div>
<div className="line-items__item__value">
+&nbsp;
{formatCurrency(getDiscountAmount(card.amount, boost), card.currency, {
hideSymbol: true
})}
</div>
</div>
)}
<div className="line-items__item">
<div className="line-items__item__label">Credit Amount</div>
<div className="line-items__item__value">
Expand All @@ -43,20 +70,19 @@ const LineItems: React.FC<{ cardConfig: CardConfig; card: Partial<GiftCard> & Un
</div>
</div>
)}
{card.discounts &&
card.discounts.map((discount, index: number) => (
<div className="line-items__item" key={index}>
<div className="line-items__item__label">
{discount.code ? `${formatDiscount(discount, cardConfig.currency, true)} ` : ''}Discount
</div>
<div className="line-items__item__value">
-&nbsp;
{formatCurrency(getDiscountAmount(card.amount, discount), card.currency, {
hideSymbol: true
})}
</div>
{discount && (
<div className="line-items__item">
<div className="line-items__item__label">
{discount.code ? `${formatDiscount(discount, cardConfig.currency, true)} ` : ''}Discount
</div>
))}
<div className="line-items__item__value">
-&nbsp;
{formatCurrency(getDiscountAmount(card.amount, discount), card.currency, {
hideSymbol: true
})}
</div>
</div>
)}
{(totalDiscount > 0 || activationFee > 0) && (
<div className="line-items__item line-items__item">
<div className={`line-items__item__label line-items__item__label${card.date ? '' : '--bold'}`}>
Expand Down
3 changes: 2 additions & 1 deletion source/popup/components/merchant-cell/merchant-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import './merchant-cell.scss';
import { Merchant } from '../../../services/merchant';
import CardDenoms from '../card-denoms/card-denoms';
import DiscountText from '../discount-text/discount-text';
import { getVisibleCoupon } from '../../../services/gift-card';

const MerchantCell: React.FC<{ merchant: Merchant }> = ({ merchant }) => {
const cardConfig = merchant.giftCards[0];
const discount = merchant.discount || (cardConfig && cardConfig.discounts && cardConfig.discounts[0]);
const discount = merchant.discount || getVisibleCoupon(cardConfig);
return (
<div className="merchant-cell">
<img className="merchant-cell__avatar" alt={`${merchant.displayName} logo`} src={merchant.icon} />
Expand Down
5 changes: 3 additions & 2 deletions source/popup/components/merchant-cta/merchant-cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import React, { useEffect } from 'react';
import './merchant-cta.scss';
import { Link } from 'react-router-dom';
import { useTracking } from 'react-tracking';
import { Merchant, getDiscount, getGiftCardDiscount, getPromoEventParams } from '../../../services/merchant';
import { Merchant, getDiscount, getPromoEventParams } from '../../../services/merchant';
import CardDenoms from '../card-denoms/card-denoms';
import SuperToast from '../super-toast/super-toast';
import DiscountText from '../discount-text/discount-text';
import { getVisibleCoupon } from '../../../services/gift-card';

const MerchantCta: React.FC<{ merchant?: Merchant; slimCTA: boolean }> = ({ merchant, slimCTA }) => {
const tracking = useTracking();
const ctaPath = merchant && (merchant.hasDirectIntegration ? `/brand/${merchant.name}` : `/amount/${merchant.name}`);
const hasDiscount = !!(merchant && getDiscount(merchant));
const hasGiftCardDiscount = !!(merchant && getGiftCardDiscount(merchant));
const hasGiftCardDiscount = !!(merchant && getVisibleCoupon(merchant.giftCards[0]));
useEffect(() => {
if (!merchant || !hasGiftCardDiscount) return;
tracking.trackEvent({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const PayWithBitpay: React.FC<Partial<RouteComponentProps> & {
});
const finalGiftCard = {
...giftCard,
discounts: cardConfig.discounts
coupons: cardConfig.coupons
} as GiftCard;
await saveGiftCard(finalGiftCard);
showCard(finalGiftCard);
Expand Down
17 changes: 17 additions & 0 deletions source/popup/pages/amount/amount.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,23 @@
.action-button__footer {
margin-top: 0;
}
.boost-amount {
color: $slateDark;
display: flex;
justify-content: center;
font-size: 14px;
margin-top: -20px;
padding: 10px;
padding-top: 0;
opacity: 0;
transform: translateY(10px);
transition: all 300ms ease;

&--visible {
opacity: 1;
transform: translateY(0);
}
}
}

&__amount-box {
Expand Down
83 changes: 55 additions & 28 deletions source/popup/pages/amount/amount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import React, { useRef, useState, Dispatch, SetStateAction, useEffect } from 're
import { RouteComponentProps } from 'react-router-dom';
import { TrackingProp } from 'react-tracking';
import { motion } from 'framer-motion';
import classNames from 'classnames';
import CardDenoms from '../../components/card-denoms/card-denoms';
import PayWithBitpay from '../../components/pay-with-bitpay/pay-with-bitpay';
import { GiftCardInvoiceParams, CardConfig, GiftCard } from '../../../services/gift-card.types';
import { getCardPrecision, isAmountValid } from '../../../services/gift-card';
import { getBoostedAmount, getCardPrecision, getVisibleCoupon, hasVisibleDiscount, hasVisibleBoost, isAmountValid, getMaxAmountWithBoost } from '../../../services/gift-card';
import DiscountText from '../../components/discount-text/discount-text';
import { Merchant } from '../../../services/merchant';
import { resizeFrame, FrameDimensions } from '../../../services/frame';
Expand All @@ -14,6 +15,7 @@ import { BitpayUser } from '../../../services/bitpay-id';
import { formatCurrency } from '../../../services/currency';
import { trackComponent } from '../../../services/analytics';
import './amount.scss';
import Snack from '../../components/snack/snack';

const shkAmp = 12;

Expand Down Expand Up @@ -52,11 +54,13 @@ const Amount: React.FC<RouteComponentProps & {
(onMerchantWebsite && isAmountValid(initialAmount || 0, cardConfig) && initialAmount) ||
(cardConfig.supportedAmounts && cardConfig.supportedAmounts[0] ? cardConfig.supportedAmounts[0] : 0);
const [amount, setAmount] = useState(preloadedAmount);
const [errorMessage, setErrorMessage] = useState('');
const [inputValue, setInputValue] = useState(
preloadedAmount
? formatCurrency(preloadedAmount, cardConfig.currency, { customPrecision: 'minimal' }).replace(/[^\d.-]/g, '')
: ''
);
const boostedAmount = formatCurrency(getBoostedAmount(cardConfig, amount), cardConfig.currency);
useEffect(() => {
if (initialAmount)
return tracking?.trackEvent({
Expand All @@ -67,21 +71,21 @@ const Amount: React.FC<RouteComponentProps & {
}, [tracking, initialAmount, cardConfig]);
const [inputError, setInputError] = useState(false);
const [inputDirty, setInputDirty] = useState(false);
const discount = (cardConfig.discounts || [])[0];
const coupon = getVisibleCoupon(cardConfig);
const invoiceParams: GiftCardInvoiceParams = {
brand: cardConfig.name,
currency: cardConfig.currency,
amount: preloadedAmount,
clientId,
discounts: discount ? [discount.code] : [],
coupons: coupon ? [coupon.code] : [],
email: (user && user.email) || email
};
const precision = getCardPrecision(cardConfig);
const baseDelta = precision === 2 ? 0.01 : 1;
const maxAmount = cardConfig.maxAmount as number;
const minAmount = cardConfig.minAmount as number;
const paymentPageAvailable =
(cardConfig.activationFees && cardConfig.activationFees.length) || discount || (!email && !user);
(cardConfig.activationFees && cardConfig.activationFees.length) || !!getVisibleCoupon(cardConfig) || (!email && !user);
const changeFixedAmount = (delta: number): void => {
const denoms = cardConfig.supportedAmounts as number[];
const maxIndex = denoms.length - 1;
Expand Down Expand Up @@ -148,16 +152,17 @@ const Amount: React.FC<RouteComponentProps & {
return tracking?.trackEvent({ action: 'changedAmount', method: 'type', gaAction: 'changedAmount:type' });
};
const goToPaymentPage = (): void => {
isAmountValid(amount, cardConfig)
? history.push({
pathname: `/payment/${cardConfig.name}`,
state: {
amount,
cardConfig,
invoiceParams
}
})
: shakeInput();
if (!isAmountValid(amount, cardConfig)) {
return;
}
history.push({
pathname: `/payment/${cardConfig.name}`,
state: {
amount: getBoostedAmount(cardConfig, amount),
cardConfig,
invoiceParams
}
})
};
const handleKeyDown = (key: number): void => {
if (paymentPageAvailable && key === 13) {
Expand All @@ -168,24 +173,40 @@ const Amount: React.FC<RouteComponentProps & {
}
setInputDirty(true);
};
const onContinue = (): void =>
cardConfig.phoneRequired || cardConfig.mobilePaymentsSupported
? history.push({
pathname: `/phone`,
state: {
amount,
cardConfig,
invoiceParams
}
})
: goToPaymentPage();
const validateAmount = () : boolean => {
if (getBoostedAmount(cardConfig, amount) > maxAmount) {
const maxAmountWithBoost = getMaxAmountWithBoost(cardConfig);
setErrorMessage(`The boosted amount must not exceed ${formatCurrency(maxAmount, cardConfig.currency, { customPrecision: 'minimal' })}. Please enter an amount of ${formatCurrency(maxAmountWithBoost!, cardConfig.currency, { customPrecision: 'minimal' })} or less.`);
shakeInput();
return false;
}
return true;
}
const onContinue = (): void => {
if (!validateAmount()) {
return;
}
if (cardConfig.phoneRequired || cardConfig.mobilePaymentsSupported) {
history.push({
pathname: `/phone`,
state: {
amount: getBoostedAmount(cardConfig, amount),
cardConfig,
invoiceParams
}
});
} else {
goToPaymentPage();
}
}
if (!initiallyCollapsed || !isFirstPage) resizeFrame(FrameDimensions.amountPageHeight);
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className="amount-page" onClick={focusInput}>
<Snack message={errorMessage} onClose={() => {setErrorMessage('')}} />
<div className="amount-page__title">
<div className="amount-page__merchant-name">{cardConfig.displayName}</div>
{discount && (
{hasVisibleDiscount(cardConfig) && (
<div className="amount-page__promo">
<DiscountText merchant={merchant} />
</div>
Expand Down Expand Up @@ -231,6 +252,12 @@ const Amount: React.FC<RouteComponentProps & {
</div>
</div>
<div className="amount-page__cta">
{hasVisibleBoost(cardConfig) && (
<div className={classNames({
'boost-amount': true,
'boost-amount--visible': amount > 0
})}>{boostedAmount} with&nbsp;<DiscountText merchant={merchant} /></div>
)}
{paymentPageAvailable ? (
<div className="action-button__footer">
<ActionButton onClick={onContinue} disabled={!amount}>
Expand All @@ -239,14 +266,14 @@ const Amount: React.FC<RouteComponentProps & {
</div>
) : (
<PayWithBitpay
invoiceParams={{ ...invoiceParams, amount }}
invoiceParams={{ ...invoiceParams, amount: getBoostedAmount(cardConfig, amount) }}
user={user}
cardConfig={cardConfig}
history={history}
purchasedGiftCards={purchasedGiftCards}
setPurchasedGiftCards={setPurchasedGiftCards}
supportedMerchant={supportedMerchant}
onInvalidParams={(): void => shakeInput()}
onInvalidParams={(): void => { validateAmount() }}
/>
)}
</div>
Expand Down
6 changes: 3 additions & 3 deletions source/popup/pages/brand/brand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Observer from '@researchgate/react-intersection-observer';
import ReactMarkdown from 'react-markdown';
import { motion } from 'framer-motion';
import { Directory, DirectoryCategory } from '../../../services/directory';
import { Merchant, getDiscount } from '../../../services/merchant';
import { Merchant, getCouponColor, getDiscount } from '../../../services/merchant';
import { resizeToFitPage, FrameDimensions } from '../../../services/frame';
import { goToPage } from '../../../services/browser';
import CardDenoms from '../../components/card-denoms/card-denoms';
Expand Down Expand Up @@ -47,7 +47,7 @@ const Brand: React.FC<RouteComponentProps & { directory: Directory }> = ({ locat
goToPage(merchant.cta.link);
tracking.trackEvent(getEventParams('clickedMerchantCta'));
};
const color = merchant.theme === '#ffffff' ? '#4f6ef7' : merchant.theme;
const color = getCouponColor(merchant);
const bubbleColor = { color, borderColor: color };
const suggested = useMemo((): { category: DirectoryCategory; suggestions: Merchant[] } => {
const category = [...directory.categories].sort(
Expand Down Expand Up @@ -182,7 +182,7 @@ const Brand: React.FC<RouteComponentProps & { directory: Directory }> = ({ locat

<Observer onChange={handleIntersection} threshold={0.8} disabled={pageEntering}>
<div>
{suggested.suggestions.slice(0, 2).map((suggestion) => (
{suggested.suggestions.slice(0, 2).map(suggestion => (
<Link
to={{
pathname: `/brand/${suggestion.name}`,
Expand Down
4 changes: 2 additions & 2 deletions source/popup/pages/payment/payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ const Payment: React.FC<RouteComponentProps & {
amount,
currency: invoiceParams.currency,
name: cardConfig.name,
discounts: cardConfig.discounts
coupons: cardConfig.coupons
};
const shouldShowLineItems = !!(
(cardConfig.discounts && cardConfig.discounts.length) ||
(cardConfig.coupons && cardConfig.coupons.length) ||
(cardConfig.activationFees && cardConfig.activationFees.length)
);
const onEmailChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
Expand Down
Loading

0 comments on commit 6b2430f

Please sign in to comment.