Package Exports
- @mojitonft/mojito-mixers
- @mojitonft/mojito-mixers/dist/cjs/src/index.js
- @mojitonft/mojito-mixers/dist/esm/src/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@mojitonft/mojito-mixers) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Mojito Payment UI
👨💻 Mojito Payment UI modal & playground UI to easily test payments with credit card, ACH, Wire and Crypto payments with Circle, 3DS, Plaid and Vertex integrations.
🚀 Check it out at https://payments-staging.mojito.xyz/!
⚠️ This is still in alpha, use with caution.
Working on the project
Next.js development playground:
While this project will be installed as a dependency in other apps, it also provides a development/test playground to speed up development and improve DX. In order to use it:
First, duplicate
app/.env
toapp/.env.local
and add the two missing values.To start the Next.js development playground:
yarn --cwd app install yarn dev
This will install the dependencies defined in
app/package.json
and run the Next.js app inside./app
.Access the project at http://localhost:3000.
Before committing, be sure to run:
yarn lint
There is also
yarn lint:fix
which can automatically fix some lint issues.Do not run
yarn deprecated:prettier
, that will be either updated or removed from the project later.
Test data and environments:
When testing the purchase flow, you need to make sure to:
Use a real
orgID
andlotID
that exists in your Mojito account.This can either be a Buy Now lot ot an Auction lot that the test user that you are going to use to make the purchase won.
You can find them using:
Mojito Mint -
https://mint.dev.mojito.xyz/
.Mojito API GraphQL Playground -
https://api.dev.mojito.xyz/query
When paying with credit card, use Circle's Test card numbers. As you can see, only Visa and MasterCard are supported.
If you want to verify the validation of other credit card networks or the functionality of the
PaymentMethodForm
in general, you can use these test card numbers.If you want to check 3DS' error handling, see 3DS in Sandbox on how to force those errors to be triggered in the sandbox environment.
When paying with ACH, refer to Plaid's - Testing OAuth documentation.
Building this project as a library
The project includes a separated Rollup build to build it as a library that can be installed and consumed by other projects.
To build the lib:
yarn install
yarn dist:build
This builds the library using the Rollup setup at the root of the project and the dependencies defined in package.json
. It does so by temporarily mv app/src/lib src
, and undoing that once the lib has been built.
GraphQL Codegen
Automatically generated types and hooks from the Mojito GraphQL API:
To update these, first ensure that you're running a local instance of
mojito-api
(or change codegen.yml
's schema
property to point to the production API) and then run:
yarn codegen
To create new queries or mutations, create a .graphql
file (for example app/src/lib/queries/me.graphql
) and then run
yarn codegen
again, and it will automatically find all .graphql
files in the repo and create typed React hooks from
them. If, for example, the query is called Organization
, then the auto-generated hook will be called useOrganizationQuery
.
Using this project as a library
Once you've built the library using yarn dist:build
, you can install it in another project with one of these options
(until it's published in NPM):
"@mojitoinc/mojito-mixers": "file:../mojito-mixers"
"@mojitoinc/mojito-mixers": "git+ssh://git@github.com/mojitoinc/mojito-mixers"
Also, make sure you install the following dependencies:
react
react-dom
@mui/material
And also, keep in mind:
@emotion/react
is not needed.@emotion/styled
is needed as stated in MUI's docs:Keep
@emotion/styled
as a dependency of your project. Even if you never use it explicitly, it's a peer dependency of@mui/material
.styled-components
is needed as stated inreact-payment-inputs
' docs, but it's not used:Note:
requires styled-components to be installed as a dependency. By default, React Payment Inputs does not have built-in styling for it's inputs. However, React Payment Inputs comes with a styled wrapper which combines the card number, expiry & CVC fields...
Usage:
In your App's entry point (_app.tsx
in Next.js), you need to add the CheckoutOverlayProvider
:
const router = useRouter();
const paymentIdParam = router.query[THREEDS_FLOW_SEARCH_PARAM_SUCCESS_KEY]?.toString();
const paymentErrorParam = router.query[THREEDS_FLOW_SEARCH_PARAM_ERROR_KEY]?.toString();
// Add any other pages where you don't want the Payment UI to be rendered:
const doNotRenderPaymentUI = ["/payments/success", "/payments/error", "/payments/failure"].includes(router.pathname);
// Debug information in case you need it:
// console.log({ pathname: router.asPath, paymentIdParam, paymentErrorParam, doNotRenderPaymentUI });
return (
<CheckoutOverlayProvider
paymentIdParam={ paymentIdParam }
paymentErrorParam={ paymentErrorParam }
checkoutComponent={ CheckoutComponent }
doNotRenderPaymentUI={ doNotRenderPaymentUI }>
{ ... }
</CheckoutOverlayProvider>
);
Where CheckoutComponent: React.FC<CheckoutComponentWithRequiredProps>
is a component you need to create that renders
PUICheckout
with your custom configuration and its props. Simply copy the following file and adapt it to your needs:
app/src/components/checkout-component/CheckoutComponent.tsx
Lastly, in the pages where you'd like to use the Payment UI, you need to use the useCheckoutOverlay
to customize and
open the Payment UI:
const { open, setCheckoutComponentProps } = useCheckoutOverlay();
const getComponentPropsRef = useRef<() => CheckoutComponentProps>(() => ({}));
getComponentPropsRef.current = () => {
return {
orgID: "<orgID>",
invoiceID: "<invoiceID>",
checkoutItems: [{
// Common:
lotID: "<lotID>", // Mojito API ID
lotType: "<lotType>",
name: "<name>",
description: "<description>",
imageSrc: "<imageSrc>",
imageBackground: "<imageBackground>",
// Buy Now:
totalSupply: "<totalSupply>",
remainingSupply: "<remainingSupply>",
units: "<units>",
// Auction:
fee: "<fee>",
}],
};
};
const handleOpenClicked = useCallback(() => open(getComponentPropsRef.current()), [open]);
useEffect(() => {
setCheckoutComponentProps(getComponentPropsRef.current());
// If some of the fields in the object above can change, add them to the dependencies here:
}, [setCheckoutComponentProps]);
You can see an example here:
Make sure you check the setup steps below for Vertex, 3DS and Plaid.
Supported Countries
We use Circle for payments, so the supported countries depend on which payment method is going to be used, as described here:
https://developers.circle.com/docs/supported-countries
We use the following script to compile the list of excluded countries:
app/src/lib/hooks/useCountryOptionsBlacklistScript.js
Address Validation & Tax Calculation with Vertex
Id you'd like address to be validated and taxes to be calculated during the checkout process, particularly in the Billing Information step, you need a Vertex account.
Alternatively, set the following prop to disable those calls to the backend: vertexEnabled = false
.
Credit Card payments with 3DS
Additionally, when using 3DS for Credit Card payments you need to add a success and error page into your app. The URL can be anything you want as long as you configure that in your 3DS account. In this repo, those pages are:
/payments/success
=> app/src/pages/payments/success.tsx./payments/error
=> app/src/pages/payments/error.tsx.
Alternatively, /payments/failure
is also valid.
You can just copy-paste those into your project as a starting point, only minor changes are needed there. As you can see,
most of the logic in those pages is already provided by this library in the
PUISuccess
and
PUIError
components.
If you don't have a 3DS account and just want to disable that step, you can do that with the following prop: threeDSEnabled = false
.
// /payments/success:
const CreditCardPaymentSuccessPage: React.FC = () => {
const router = useRouter();
const handleRedirect = useCallback((pathnameOrUrl: string) => {
if (pathnameOrUrl && pathnameOrUrl.startsWith("http")) {
window.location.replace(pathnameOrUrl);
} else {
router.replace(pathnameOrUrl || "/");
}
}, [router]);
return (
<PUISuccess
themeOptions={ YOUR_CUSTOM_THEME_OPTIONS }
logoSrc="https://..."
logoSx={ ... }
successImageSrc="https://..."
onRedirect={ handleRedirect } />
);
};
export default CreditCardPaymentSuccessPage;
// /payments/error:
const CreditCardPaymentErrorPage: React.FC = () => {
const router = useRouter();
const handleRedirect = useCallback((pathnameOrUrl: string) => {
if (pathnameOrUrl && pathnameOrUrl.startsWith("http")) {
window.location.replace(pathnameOrUrl);
} else {
router.replace(pathnameOrUrl || "/");
}
}, [router]);
return (
<PUIError
themeOptions={ YOUR_CUSTOM_THEME_OPTIONS }
logoSrc="https://..."
logoSx={ ... }
errorImageSrc="https://..."
onRedirect={ handleRedirect } />
);
};
export default CreditCardPaymentErrorPage;
ACH payments with Plaid:
Additionally, when using Plaid for ACH payments you need to add an /oauth
page with the following logic to be able
to resume Plaid's OAuth flow when users are redirected back to your app:
const PlaidOAuthPage = () => {
const router = useRouter();
const { continueOAuthFlow, url } = getPlaidOAuthFlowState();
useLayoutEffect(() => {
if (continueOAuthFlow) {
persistPlaidReceivedRedirectUri(window.location.href);
}
router.replace(url || "/");
}, [continueOAuthFlow, router, url]);
return null;
};
export default PlaidOAuthPage;
Theming
You can use the themeOptions
or theme
props to pass a custom theme or theme options object:
themeOptions
(preferred) will merge Mojito's default theme with your custom one.<PUICheckout themeOptions={ YOUR_CUSTOM_THEME_OPTIONS } { ...checkoutModalProps } />
theme
will completely replace Mojito's default theme with the one you provide.<PUICheckout theme={ YOUR_CUSTOM_THEME } { ...checkoutModalProps } />
See
- If none is provided, the default Mojito theme will be used.
Note that using MUI's ThemeProvider
from your project won't work as expected and you will end up seeing Mojito's default theme:
<ThemeProvider theme={ YOUR_CUSTOM_THEME }>
<PUICheckout { ...checkoutModalProps } />
</ThemeProvider>
Dictionary
There are some texts inside the Payment UI that you can customize using PUICheckout
's dictionary
prop (more to come, ideally all texts should be customizable). You can find them all with their respective default values here:
app/src/lib/domain/dictionary/dictionary.constants.tsx
.
Errors, Exceptions and Validation Messages
Error, exception and validation messages in the Payment UI are displayed in the ErrorView
and have a configurable button text and action (what the button does or where it takes users when clicking it). Particularly,
those actions are:
reset
: Re-creates the reservation/invoice.authentication
: Takes users to the authentication view (currently not used).billing
: Takes users to the billing view/form.payment
: Takes users to the payment view/form.purchasing
: Takes users to the purchasing view and re-tries payment.
Error messages
Defined in app/src/lib/domain/errors/errors.constants.ts
:
ERROR_GENERIC =
An unexpected error happened.action = payment
ERROR_LOADING =
Loading error details...action = none
ERROR_LOADING_USER =
User could not be loaded.action = billing
ERROR_LOADING_PAYMENT_METHODS =
Payment methods could not be loaded.action = billing
ERROR_LOADING_INVOICE =
Invoice could not be loaded.action = billing
ERROR_PURCHASE =
The purchase could not be completed.action = payment
ERROR_PURCHASE_TIMEOUT =
The purchase could not be completed in time.action = payment
ERROR_PURCHASE_NO_ITEMS =
No items to purchase.action = payment
ERROR_PURCHASE_NO_UNITS =
No units to purchase.action = payment
ERROR_PURCHASE_LOADING_ITEMS =
Purchase items could not be loaded.action = payment
ERROR_PURCHASE_SELECTED_PAYMENT_METHOD =
Could not find the selected payment method.action = payment
ERROR_PURCHASE_CREATING_PAYMENT_METHOD =
Payment method could not be saved.action = billing
ERROR_PURCHASE_CREATING_INVOICE =
Invoice could not be created.action = reset
ERROR_PURCHASE_CVV =
Could not verify CVV.action = payment
ERROR_PURCHASE_PAYING =
Payment failed.action = payment
ERROR_PURCHASE_3DS =
Payment method could not be verified.action = payment
ERROR_INVOICE_TIMEOUT =
Your product reservation expired. Please, try to complete the purchase again in time.action = reset
Additionally, there are some backend errors that are mapped to frontend ones:
lot auction not started =
The auction has not started yet.action = reset
payment limit exceeded =
You have already bought the maximum number of NFTs allowed for this sale.action = reset
name should contains first and last name =
Full Name must have at least first and last name.action = billing
Exceptions messages
Defined in app/src/lib/domain/errors/exceptions.constants.ts
:
DEV.THEME_PROVIDER =
(DEV) You can't use boththemeOptions
andtheme
. Please, use only one.themeOptions
is preferred.DEV.APOLLO_PROVIDER_DUPLICATE =
(DEV) You can't use bothapolloClient
anduri
. Please, use only one.uri
is preferred.DEV.APOLLO_PROVIDER_MISSING =
(DEV) You must setapolloClient
oruri
. Please, add one.uri
is preferred.DEV.ENCRYPTION_KEYS_MISSING =
(DEV) MissingpublicKey
orkeyID
.PAYMENT_METHOD.UNSUPPORTED =
Unsupported payment method.PAYMENT_METHOD.CREATION_FAILED =
Payment method could not be created.PAYMENT_METHOD.VALIDATION_FAILED =
Payment method could not be validated.PAYMENT_METHOD.VALIDATION_TIMEOUT =
Payment method validation took too long.
Note those prefixed with (DEV)
will never be shown to regular users. Instead, they will see the ERROR_GENERIC
from above.
Validation Messages
Defined in app/src/lib/utils/validationUtils.ts
:
withRequiredErrorMessage =
{ label }
is required.withInvalidErrorMessage =
{ label }
is not valid.CONSENT_ERROR_MESSAGE =
You must accept the terms and conditions of the sale.withFullNameErrorMessage =
{ label }
must have at least first and last name.withFullNameCharsetErrorMessage =
{ label }
contains invalid characters.withPhoneErrorMessage =
{ label }
must be a valid phone number.SELECTION_ERROR_MESSAGE =
You must select a saved and approved payment method or create a new one.withInvalidAddress =
Please,{ enter/select }
a valid address to calculate taxes.withInvalidZipCode =
The{ label }
you entered does not match the address.withInvalidCardNumber =
{ label }
is invalid.withInvalidCVV =
{ cvvLabel }
must have{ cvvExpectedLength }
digits.withInvalidCreditCardNetwork =
Only{ acceptedCreditCardNetworks }
{ is/are }
accepted.withInvalidConnection =
Could not connect{ label }
.
(Secret) Debug Mode
If you quickly click the logo in the top-right corner 16 times, the debug mode will be enabled (toggled, actually), even in production and regardless of
the initial value you passed for the debug
prop.
The debug mode will, among logging/displaying some other less relevant pieces of data:
Show form values and errors as JSON below the form:
Show additional logging information for the most relevant queries/mutation being made:
TypeScript Support
You will have to add the following file into your project to avoid TypeScript errors when using custom props in MUI's theme:
import "@mui/material/styles";
import { PalettePaymentUI } from "@mojitoinc/mojito-mixers";
declare module "@mui/material/styles" {
interface Palette {
paymentUI?: PalettePaymentUI;
}
interface PaletteOptions {
paymentUI?: PalettePaymentUI;
}
}
You can see an example here: app/src/lib/domain/mui/mui.d.ts
Error Handling
All components exported by this library are wrapped in a custom ErrorBoundary
so that, in the event of an unexpected error in the library, it doesn't crash your app as well. You can find it here:
app/src/lib/components/public/ErrorBoundary/ErrorBoundary.tsx
.
By default, if an unexpected error occurs, a confirm window/modal will be presented to the users asking them if they want to re-open the Payment UI:
If you don't want this behavior or would like to implement a custom one, you should pass a value for onCatch: (error: Error, errorInfo?: ErrorInfo) => void | true;
prop with a callback. If you want to get notified about unexpected errors but would still like to preserve the default behavior, return true
from your callback.
onEvent
callback prop:
The onEvent
callback prop can be used to get updates about the progress of the user using the Payment UI, which can be useful for analytics:
onEvent?: (eventType: CheckoutEventType, eventData: CheckoutEventData) => void;
eventType: CheckoutEventType
values:
Events triggered when the user sees a specific view:
navigate:authentication
navigate:billing
navigate:payment
navigate:purchasing
navigate:confirmation
navigate:error
Events triggered when the user performs a specific action:
event:paymentSuccess
: The "Purchase" button in the Payment view has been clicked and the payment has been made successfully.event:paymentError
: The "Purchase" button in the Payment view has been clicked and the payment has been attempted, but it failed.
eventData: Partial<CheckoutEventData>
props:
All events will provide this data, but notice some properties are optional, as they might not be available for all steps:
interface CheckoutEventData {
// auth0ID: string; // Not added, already on the parent.
// checkoutType: string; // Not added, already on the parent.
// customerId: string; // Not added, already on the parent.
// Location:
step: number;
stepName: string;
// Purchase:
departmentCategory: "NFT";
paymentType?: PaymentType; // "CreditCard" | "ACH" | "Wire" | "Crypto"
shippingMethod: ShippingMethod; // "custom wallet" | "multisig wallet"
checkoutItems: CheckoutItem[]; // Provided as this might be a mix of the checkoutItems prop and some additional data from the invoice.
// Payment:
currency: "USD";
revenue: number; // Revenue (subtotal) associated with the transaction, excluding shipping and taxes.
fees: number;
tax?: number;
total: number; // Total value of the order with discounts, taxes and fees.
// Order:
processorPaymentID?: string; // Can be used as orderID.
paymentID?: string; // Can be used as orderID.
}
Images
The following images are loaded directly from GitHub to avoid bundling them with the library or forcing users to include them in their repos and add the necessary build setup to load them. They should just work out of the box, no setup required:
PurchasingView
's default loader image.ErrorView
's default error image.PaymentView
's Circle logo image.
PurchaseView
's default loader image:
> Repo: https://github.com/mojitoinc/mojito-mixers/blob/main/app/src/lib/assets/mojito-loader.gif (add `?raw=true` to get the CDN URL below)
> CDN URL: https://raw.githubusercontent.com/mojitoinc/mojito-mixers/main/app/src/lib/assets/mojito-loader.gif
ErrorView
's default error image:
> Repo: https://github.com/mojitoinc/mojito-mixers/blob/main/app/src/lib/assets/mojito-error-loader.gif (add `?raw=true` to get the CDN URL below)
> CDN URL: https://raw.githubusercontent.com/mojitoinc/mojito-mixers/main/app/src/lib/assets/mojito-error-loader.gif
Alternative static version:
> Repo: https://github.com/mojitoinc/mojito-mixers/blob/main/app/src/lib/assets/mojito-error-loader-static.png (add `?raw=true` to get the CDN URL below)
> CDN URL: https://raw.githubusercontent.com/mojitoinc/mojito-mixers/main/app/src/lib/assets/mojito-error-loader-static.png
PaymentView
's Circle logo image:
> Repo: https://github.com/mojitoinc/mojito-mixers/blob/main/app/src/lib/assets/circle.png (add `?raw=true` to get the CDN URL below)
> CDN URL: https://raw.githubusercontent.com/mojitoinc/mojito-mixers/main/app/src/lib/assets/circle.png