Skip to content

Commit

Permalink
Merge branch 'main' into travjenkins/bindings/notBeforeNotAfter
Browse files Browse the repository at this point in the history
  • Loading branch information
travjenkins committed Oct 10, 2023
2 parents e5c539c + cd4e380 commit 597cbaf
Show file tree
Hide file tree
Showing 38 changed files with 774 additions and 208 deletions.
20 changes: 15 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"logrocket": "^3.0.1",
"logrocket-react": "^5.0.1",
"lru-cache": "^9.1.2",
"luxon": "^3.4.3",
"madge": "^5.0.2",
"material-ui-popup-state": "^5.0.9",
"monaco-editor": "^0.34.1",
Expand Down Expand Up @@ -104,6 +105,7 @@
"@types/auth0-js": "^9.14.7",
"@types/lodash": "^4.14.191",
"@types/logrocket-react": "^3.0.0",
"@types/luxon": "^3.3.2",
"@types/react": "^17.0.52",
"@types/react-dom": "^17.0.18",
"@types/react-gtm-module": "^2.0.1",
Expand Down
34 changes: 34 additions & 0 deletions src/api/billing.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { PostgrestResponse } from '@supabase/postgrest-js';
import pLimit from 'p-limit';
import {
FUNCTIONS,
TABLES,
invokeSupabase,
supabaseClient,
} from 'services/supabase';
import { Tenants } from 'types';
import { formatDateForApi } from 'utils/billing-utils';

const OPERATIONS = {
Expand Down Expand Up @@ -124,3 +126,35 @@ export const getInvoicesBetween = (
.order('date_start', { ascending: false })
.throwOnError();
};

export interface MultiplePaymentMethods {
responses: any[];
errors: any[];
}

// Very few people are using multiple prefixes (Q4 2023) so allowing us to check 5 for now
// is more than enough. This also prevents people in the support role from hammering the server
// fetching payment methods for tenants they do now "own"
const MAX_TENANTS = 5;
export const getPaymentMethodsForTenants = async (
tenants: Tenants[]
): Promise<MultiplePaymentMethods> => {
const limiter = pLimit(3);
const promises: Array<Promise<any>> = [];
let count = 0;

tenants.some((tenantDetail) => {
promises.push(
limiter(() => getTenantPaymentMethods(tenantDetail.tenant))
);
count += 1;
return count >= MAX_TENANTS;
});

const responses = await Promise.all(promises);

return {
responses: responses.filter((r) => r.data).map((r) => r.data),
errors: responses.filter((r) => r.error).map((r) => r.error),
};
};
9 changes: 7 additions & 2 deletions src/api/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ type AllowedDates = Date | string | number;
// Make sure that this matched the derivation closely
// Function : grainsFromTS
// Source : https://github.com/estuary/flow/blob/master/ops-catalog/catalog-stats.ts
export const convertToUTC = (date: AllowedDates, grain: Grains) => {
// TODO (typing)
export const convertToUTC = (
date: AllowedDates,
grain: Grains,
skipConversion?: boolean
): any => {
const isoUTC = new UTCDate(
typeof date === 'string' ? parseISO(date) : date
);
Expand All @@ -115,7 +120,7 @@ export const convertToUTC = (date: AllowedDates, grain: Grains) => {
isoUTC.setUTCDate(1);
}

return isoUTC.toISOString();
return skipConversion ? isoUTC : isoUTC.toISOString();
};

// TODO (stats) add support for which stats columns each entity wants
Expand Down
14 changes: 5 additions & 9 deletions src/api/tenants.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { supabaseClient, TABLES } from 'services/supabase';
import { Tenants } from 'types';

const getTenantDetails = () => {
const queryBuilder = supabaseClient.from<Tenants>(TABLES.TENANTS).select(
`
tasks_quota,
collections_quota,
tenant
`
);
const COLUMNS = ['tasks_quota', 'collections_quota', 'tenant', 'trial_start'];

return queryBuilder;
const getTenantDetails = () => {
return supabaseClient
.from<Tenants>(TABLES.TENANTS)
.select(COLUMNS.join(','));
};

export { getTenantDetails };
5 changes: 5 additions & 0 deletions src/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ const admin = {
title: 'routeTitle.admin.billing',
path: 'billing',
fullPath: '/admin/billing',
addPayment: {
title: 'routeTitle.admin.billing',
path: `paymentMethod/new`,
fullPath: '/admin/billing/paymentMethod/new',
},
},
settings: {
title: 'routeTitle.admin.settings',
Expand Down
91 changes: 91 additions & 0 deletions src/components/admin/Billing/AddPaymentMethod.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { LoadingButton } from '@mui/lab';
import { Box, Dialog, DialogTitle } from '@mui/material';
import { Elements } from '@stripe/react-stripe-js';
import { Stripe } from '@stripe/stripe-js';
import { setTenantPrimaryPaymentMethod } from 'api/billing';
import { PaymentForm } from 'components/admin/Billing/CapturePaymentMethod';
import { Plus } from 'iconoir-react';

import { FormattedMessage } from 'react-intl';
import { INTENT_SECRET_ERROR, INTENT_SECRET_LOADING } from './shared';

interface Props {
show: boolean;
setupIntentSecret: string;
setOpen: (val: boolean) => void;
onSuccess: () => void;
stripePromise: Promise<Stripe | null>;
tenant: string;
}

function AddPaymentMethod({
onSuccess,
show,
setupIntentSecret,
setOpen,
stripePromise,
tenant,
}: Props) {
const enableButton =
setupIntentSecret !== INTENT_SECRET_LOADING &&
setupIntentSecret !== INTENT_SECRET_ERROR;

return (
<>
<Box>
<LoadingButton
loadingPosition="start"
disabled={!enableButton}
loading={setupIntentSecret === INTENT_SECRET_LOADING}
onClick={() => setOpen(true)}
startIcon={<Plus style={{ fontSize: 15 }} />}
sx={{ whiteSpace: 'nowrap' }}
variant="contained"
>
<span>
<FormattedMessage id="admin.billing.paymentMethods.cta.addPaymentMethod" />
</span>
</LoadingButton>
</Box>

<Dialog
maxWidth="sm"
fullWidth
sx={{ padding: 2 }}
open={show}
onClose={() => setOpen(false)}
>
<DialogTitle>
<FormattedMessage id="admin.billing.addPaymentMethods.title" />
</DialogTitle>
{setupIntentSecret ? (
<Elements
stripe={stripePromise}
options={{
clientSecret: setupIntentSecret,
loader: 'auto',
}}
>
{!tenant ? null : (
<PaymentForm
onSuccess={async (id) => {
if (id) {
await setTenantPrimaryPaymentMethod(
tenant,
id
);
}
setOpen(false);
onSuccess();
}}
onError={console.log}
/>
)}
</Elements>
) : null}
</Dialog>
</>
);
}

export default AddPaymentMethod;
67 changes: 63 additions & 4 deletions src/components/admin/Billing/CapturePaymentMethod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {
useStripe,
} from '@stripe/react-stripe-js';
import { Auth } from '@supabase/ui';
import { useCallback, useState } from 'react';
import AlertBox from 'components/shared/AlertBox';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import { CustomEvents, logRocketEvent } from 'services/logrocket';
import { getUserDetails } from 'services/supabase';

export interface PaymentFormProps {
Expand All @@ -21,15 +24,60 @@ export interface PaymentFormProps {
}

export const PaymentForm = ({ onSuccess, onError }: PaymentFormProps) => {
const intl = useIntl();

const stripe = useStripe();
const elements = useElements();

const { user } = Auth.useUser();
const { email } = getUserDetails(user);

const setupEvents = useRef(false);
const [error, setError] = useState('');
const [loadingError, setLoadingError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

// Handle errors when stripe is loading in the forms
useEffect(() => {
if (setupEvents.current || !elements) {
return;
}

// Try to fetch botht the elements we're gonna need to make sure load
const addressElement = elements.getElement('address');
const paymentElement = elements.getElement('payment');
if (!addressElement || !paymentElement) {
return;
}

// Wire up handlers
paymentElement.on('loaderror', () => {
setLoadingError(
intl.formatMessage({
id: 'admin.billing.addPaymentMethods.stripeLoadError',
})
);

logRocketEvent(CustomEvents.STRIPE_FORM_LOADING_FAILED, {
formName: 'payment',
});
});
addressElement.on('loaderror', () => {
setLoadingError(
intl.formatMessage({
id: 'admin.billing.addPaymentMethods.stripeLoadError',
})
);

logRocketEvent(CustomEvents.STRIPE_FORM_LOADING_FAILED, {
formName: 'address',
});
});

// Set so we only do this once
setupEvents.current = true;
}, [elements, intl]);

const handleSubmit = useCallback(async () => {
if (!stripe || !elements) {
// Stripe.js has not yet loaded.
Expand Down Expand Up @@ -60,7 +108,10 @@ export const PaymentForm = ({ onSuccess, onError }: PaymentFormProps) => {
setError(result.error.message);
}
// Show error to your customer (for example, payment details incomplete)
await onError?.(result.error.message ?? 'Something went wrong');
await onError?.(
result.error.message ??
intl.formatMessage({ id: 'common.missingError' })
);
elements.getElement('payment')?.update({ readOnly: false });
} else {
// Your customer will be redirected to your `return_url`. For some payment
Expand All @@ -73,11 +124,16 @@ export const PaymentForm = ({ onSuccess, onError }: PaymentFormProps) => {
} finally {
setLoading(false);
}
}, [elements, email, onError, onSuccess, stripe]);
}, [elements, email, intl, onError, onSuccess, stripe]);

return (
<>
<DialogContent sx={{ overflowY: 'scroll' }}>
{loadingError ? (
<AlertBox short severity="error">
{loadingError}
</AlertBox>
) : null}
<AddressElement
options={{
mode: 'billing',
Expand Down Expand Up @@ -115,7 +171,10 @@ export const PaymentForm = ({ onSuccess, onError }: PaymentFormProps) => {
{error}
</Typography>
) : null}
<Button onClick={handleSubmit} disabled={loading}>
<Button
onClick={handleSubmit}
disabled={Boolean(loading || loadingError)}
>
{loading ? <CircularProgress size={15} /> : 'Submit'}
</Button>
</DialogActions>
Expand Down
Loading

0 comments on commit 597cbaf

Please sign in to comment.