-
Notifications
You must be signed in to change notification settings - Fork 152
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14654 from artsy/invoice-app
feat: add InvoiceApp to retrieve an invoice by token, and allow a payment
- Loading branch information
Showing
22 changed files
with
1,821 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { Column, GridColumns, Input } from "@artsy/palette" | ||
import { CountrySelect } from "Components/CountrySelect" | ||
import { Address } from "Components/Address/AddressForm" | ||
import { useFormContext } from "Apps/Invoice/Hooks/useFormContext" | ||
|
||
export interface AddressFormValues { | ||
address: Address | ||
creditCard?: boolean | ||
} | ||
|
||
export const AddressForm = () => { | ||
const { handleChange, handleBlur, errors, values, touched } = useFormContext() | ||
|
||
return ( | ||
<GridColumns> | ||
<Column span={12}> | ||
<Input | ||
name="address.name" | ||
title="Full Name" | ||
placeholder="Enter name" | ||
autoComplete="name" | ||
autoFocus | ||
value={values.address?.name} | ||
onChange={handleChange} | ||
onBlur={handleBlur} | ||
error={touched.address?.name && errors.address?.name} | ||
required | ||
/> | ||
</Column> | ||
|
||
<Column span={6}> | ||
<CountrySelect | ||
name="address.country" | ||
title="Country" | ||
// TODO: Accept a value prop in Select | ||
// @ts-ignore | ||
value={values.address.country} | ||
onChange={handleChange} | ||
onBlur={handleBlur} | ||
error={touched.address?.country && errors.address?.country} | ||
required | ||
// FIXME: There's extra margin between title and select in palette | ||
// than the title and select in input. Open PR to palette | ||
mt={-0.5} | ||
/> | ||
</Column> | ||
|
||
<Column span={6}> | ||
<Input | ||
name="address.postalCode" | ||
title="Postal Code" | ||
placeholder="Add postal code" | ||
autoComplete="postal-code" | ||
value={values.address?.postalCode} | ||
onChange={handleChange} | ||
onBlur={handleBlur} | ||
error={touched.address?.postalCode && errors.address?.postalCode} | ||
required | ||
/> | ||
</Column> | ||
|
||
<Column span={6}> | ||
<Input | ||
name="address.addressLine1" | ||
title="Address Line 1" | ||
placeholder="Add address" | ||
autoComplete="address-line1" | ||
value={values.address?.addressLine1} | ||
onChange={handleChange} | ||
onBlur={handleBlur} | ||
error={touched.address?.addressLine1 && errors.address?.addressLine1} | ||
required | ||
/> | ||
</Column> | ||
|
||
<Column span={6}> | ||
<Input | ||
name="address.addressLine2" | ||
title="Address Line 2" | ||
placeholder="Add address line 2" | ||
autoComplete="address-line2" | ||
value={values.address?.addressLine2} | ||
onChange={handleChange} | ||
onBlur={handleBlur} | ||
error={touched.address?.addressLine2 && errors.address?.addressLine2} | ||
/> | ||
</Column> | ||
|
||
<Column span={6}> | ||
<Input | ||
name="address.city" | ||
title="City" | ||
placeholder="Enter city" | ||
autoComplete="address-level2" | ||
value={values.address?.city} | ||
onChange={handleChange} | ||
onBlur={handleBlur} | ||
error={touched.address?.city && errors.address?.city} | ||
required | ||
/> | ||
</Column> | ||
|
||
<Column span={6}> | ||
<Input | ||
name="address.region" | ||
title="State, Province, or Region" | ||
placeholder="Add state, province, or region" | ||
autoComplete="address-level1" | ||
value={values.address?.region} | ||
onChange={handleChange} | ||
onBlur={handleBlur} | ||
error={touched.address?.region && errors.address?.region} | ||
required | ||
/> | ||
</Column> | ||
</GridColumns> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { Join, Spacer } from "@artsy/palette" | ||
import { CreditCardInput } from "Components/CreditCardInput" | ||
import { AddressForm } from "./AddressForm" | ||
import { useFormContext } from "Apps/Invoice/Hooks/useFormContext" | ||
|
||
export const AddressFormWithCreditCard: React.FC = () => { | ||
const { | ||
setFieldValue, | ||
setFieldTouched, | ||
setFieldError, | ||
errors, | ||
touched, | ||
} = useFormContext() | ||
|
||
return ( | ||
<Join separator={<Spacer y={2} />}> | ||
<CreditCardInput | ||
error={touched.creditCard && errors.creditCard} | ||
onChange={event => { | ||
setFieldTouched("creditCard", true) | ||
|
||
if (event.error?.message) { | ||
setFieldValue("creditCard", false) | ||
setFieldError("creditCard", event.error?.message) | ||
return | ||
} | ||
if (!event.complete) { | ||
setFieldValue("creditCard", false) | ||
return | ||
} | ||
if (event.complete) { | ||
setFieldValue("creditCard", true) | ||
return | ||
} | ||
}} | ||
required | ||
/> | ||
|
||
<AddressForm /> | ||
</Join> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { Box, Join, Separator, Text } from "@artsy/palette" | ||
import { graphql, useFragment } from "react-relay" | ||
import { InvoiceLineItems_invoice$key } from "__generated__/InvoiceLineItems_invoice.graphql" | ||
|
||
interface InvoiceLineItemsProps { | ||
invoice: InvoiceLineItems_invoice$key | ||
} | ||
|
||
export const InvoiceLineItems: React.FC<InvoiceLineItemsProps> = ({ | ||
invoice, | ||
}) => { | ||
const data = useFragment(InvoiceLineItemsFragment, invoice) | ||
|
||
return ( | ||
<Box my={4}> | ||
<Text variant="lg" fontWeight="bold"> | ||
Items | ||
</Text> | ||
|
||
<Join separator={<Separator my={2} />}> | ||
{data.lineItems.map(({ description, amount }, index) => ( | ||
<> | ||
<Box | ||
display="flex" | ||
justifyContent="space-between" | ||
mt={index === 0 ? 2 : 1} | ||
> | ||
<Text variant="sm">{description}</Text> | ||
<Text variant="sm">{amount}</Text> | ||
</Box> | ||
</> | ||
))} | ||
</Join> | ||
</Box> | ||
) | ||
} | ||
|
||
const InvoiceLineItemsFragment = graphql` | ||
fragment InvoiceLineItems_invoice on Invoice { | ||
lineItems { | ||
description | ||
amount(precision: 2) | ||
} | ||
} | ||
` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { Button } from "@artsy/palette" | ||
import { AddressFormValues } from "Apps/Invoice/Components/AddressForm" | ||
import { AddressFormWithCreditCard } from "Apps/Invoice/Components/AddressFormWithCreditCard" | ||
import { useCreateTokenAndSubmit } from "Apps/Invoice/Hooks/useCreateTokenAndSubmit" | ||
import { emptyAddress } from "Components/Address/AddressForm" | ||
import { Formik, Form } from "formik" | ||
import { useRouter } from "System/Hooks/useRouter" | ||
|
||
export interface InvoicePaymentFormProps { | ||
invoiceID: string | ||
invoiceToken: string | ||
amountMinor: number | ||
} | ||
|
||
export const InvoicePaymentForm: React.FC<InvoicePaymentFormProps> = props => { | ||
const { match, router } = useRouter() | ||
const token = match.params.token | ||
const invoiceRoute = `/invoice/${token}` | ||
const onSuccess = () => { | ||
router.push(invoiceRoute) | ||
} | ||
const { createToken: handleSubmit } = useCreateTokenAndSubmit({ | ||
onSuccess, | ||
...props, | ||
}) | ||
|
||
return ( | ||
<Formik<AddressFormValues> | ||
onSubmit={handleSubmit} | ||
initialValues={{ address: emptyAddress, creditCard: false }} | ||
> | ||
{({ isSubmitting, isValid }) => { | ||
return ( | ||
<Form> | ||
<AddressFormWithCreditCard /> | ||
|
||
<Button | ||
mt={2} | ||
size="large" | ||
width="100%" | ||
loading={isSubmitting} | ||
disabled={!isValid} | ||
type="submit" | ||
> | ||
Pay now | ||
</Button> | ||
</Form> | ||
) | ||
}} | ||
</Formik> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { Box, Column, GridColumns, Join, Separator, Text } from "@artsy/palette" | ||
import { graphql, useFragment } from "react-relay" | ||
import { InvoicePayments_invoice$key } from "__generated__/InvoicePayments_invoice.graphql" | ||
|
||
interface InvoiceLineItemsProps { | ||
invoice: InvoicePayments_invoice$key | ||
} | ||
|
||
export const InvoicePayments: React.FC<InvoiceLineItemsProps> = ({ | ||
invoice, | ||
}) => { | ||
const data = useFragment(InvoicePaymentsFragment, invoice) | ||
|
||
return ( | ||
<Box my={4}> | ||
<Text variant="lg" fontWeight="bold"> | ||
Payments | ||
</Text> | ||
|
||
<Join separator={<Separator />}> | ||
{data.payments | ||
.filter(({ successful }) => successful) | ||
.map(({ id, createdAt, amount, creditCard }, index) => { | ||
const creditCardInfo = creditCard | ||
? `${creditCard.brand} ending in ${creditCard.lastDigits}` | ||
: null | ||
return ( | ||
<> | ||
<GridColumns my={2} key={id}> | ||
<Column span={2}> | ||
<Text variant="sm">{createdAt}</Text> | ||
</Column> | ||
<Column span={6}> | ||
{creditCardInfo && ( | ||
<Text variant="sm" textAlign={"center"}> | ||
{creditCardInfo} | ||
</Text> | ||
)} | ||
</Column> | ||
<Column span={4}> | ||
<Text variant="sm" textAlign={"right"}> | ||
{amount} | ||
</Text> | ||
</Column> | ||
</GridColumns> | ||
</> | ||
) | ||
})} | ||
</Join> | ||
</Box> | ||
) | ||
} | ||
|
||
const InvoicePaymentsFragment = graphql` | ||
fragment InvoicePayments_invoice on Invoice { | ||
payments { | ||
id | ||
successful | ||
createdAt(format: "MMM D, YYYY") | ||
amount(precision: 2) | ||
creditCard { | ||
brand | ||
lastDigits | ||
} | ||
} | ||
} | ||
` |
Oops, something went wrong.