Skip to content

Commit

Permalink
create metadata field (#33)
Browse files Browse the repository at this point in the history
* create metadata field

* update DaimoPayIntentStatus values

* create externalId field for unique order ids

* better assert logging
  • Loading branch information
andrewliu08 authored Feb 18, 2025
1 parent f0b7bb9 commit 6ca9f5a
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 130 deletions.
10 changes: 5 additions & 5 deletions packages/connectkit/src/components/DaimoPay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,12 @@ const DaimoPayProviderWithoutSolana = ({
// Order just updated...
if (daimoPayOrder?.mode !== DaimoPayOrderMode.HYDRATED) return;

const { sourceStatus, intentStatus } = daimoPayOrder;
const { intentStatus } = daimoPayOrder;
let intervalMs = 0;
if (sourceStatus === DaimoPayOrderStatusSource.WAITING_PAYMENT) {
intervalMs = 2500; // additional, faster polling in WaitingOther
} else if (intentStatus === DaimoPayIntentStatus.PENDING) {
intervalMs = 300; // poll fast from (payment initiated) to (finished)
if (intentStatus === DaimoPayIntentStatus.UNPAID) {
intervalMs = 2000; // additional, faster polling in WaitingOther
} else if (intentStatus === DaimoPayIntentStatus.STARTED) {
intervalMs = 300; // poll fast from payment started to payment completed
} else {
return;
}
Expand Down
73 changes: 47 additions & 26 deletions packages/connectkit/src/components/DaimoPayButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
assertNotNull,
DaimoPayIntentStatus,
DaimoPayOrderMode,
DaimoPayOrderStatusSource,
DaimoPayUserMetadata,
getDaimoPayOrderView,
PaymentBouncedEvent,
PaymentCompletedEvent,
PaymentStartedEvent,
Expand Down Expand Up @@ -64,6 +65,14 @@ type PayButtonPaymentProps =
* Preferred tokens. These appear first in the token list.
*/
preferredTokens?: { chain: number; address: Address }[];
/**
* External ID. E.g. a correlation ID.
*/
externalId?: string;
/**
* Developer metadata. E.g. correlation ID.
* */
metadata?: DaimoPayUserMetadata;
}
| {
/** The payment ID, generated via the Daimo Pay API. Replaces params above. */
Expand Down Expand Up @@ -154,6 +163,8 @@ function DaimoPayButtonCustom(props: DaimoPayButtonCustomProps) {
paymentOptions: props.paymentOptions,
preferredChains: props.preferredChains,
preferredTokens: props.preferredTokens,
externalId: props.externalId,
metadata: props.metadata,
}
: null;
let payId = "payId" in props ? props.payId : null;
Expand All @@ -178,9 +189,8 @@ function DaimoPayButtonCustom(props: DaimoPayButtonCustomProps) {
const { onPaymentStarted, onPaymentCompleted, onPaymentBounced } = props;

const order = paymentState.daimoPayOrder;
const intentStatus = order?.intentStatus;
const hydOrder = order?.mode === DaimoPayOrderMode.HYDRATED ? order : null;
const isStarted =
hydOrder?.sourceStatus !== DaimoPayOrderStatusSource.WAITING_PAYMENT;

// Functions to show and hide the modal
const { children, closeOnSuccess } = props;
Expand All @@ -191,33 +201,44 @@ function DaimoPayButtonCustom(props: DaimoPayButtonCustomProps) {
};
const hide = () => context.setOpen(false);

useEffect(() => {
if (hydOrder == null || !isStarted) return;
onPaymentStarted?.({
paymentId: writeDaimoPayOrderID(hydOrder.id),
type: "payment_started",
chainId: assertNotNull(hydOrder.sourceTokenAmount).token.chainId,
txHash: assertNotNull(hydOrder.sourceInitiateTxHash),
});
}, [isStarted]);

// Emit event handlers when payment status changes
useEffect(() => {
if (hydOrder == null) return;
if (hydOrder.intentStatus === DaimoPayIntentStatus.PENDING) return;
if (intentStatus === DaimoPayIntentStatus.UNPAID) return;

const commonFields = {
paymentId: writeDaimoPayOrderID(hydOrder.id),
chainId: assertNotNull(hydOrder.destFinalCallTokenAmount).token.chainId,
txHash: assertNotNull(
hydOrder.destFastFinishTxHash ?? hydOrder.destClaimTxHash,
),
};
if (hydOrder.intentStatus === DaimoPayIntentStatus.SUCCESSFUL) {
onPaymentCompleted?.({ type: "payment_completed", ...commonFields });
} else if (hydOrder.intentStatus === DaimoPayIntentStatus.REFUNDED) {
onPaymentBounced?.({ type: "payment_bounced", ...commonFields });
if (intentStatus === DaimoPayIntentStatus.STARTED) {
onPaymentStarted?.({
type: DaimoPayIntentStatus.STARTED,
paymentId: writeDaimoPayOrderID(hydOrder.id),
chainId: hydOrder.destFinalCallTokenAmount.token.chainId,
txHash: assertNotNull(
hydOrder.sourceInitiateTxHash,
`[PAY BUTTON] source initiate tx hash null on order ${hydOrder.id} when intent status is ${intentStatus}`,
),
payment: getDaimoPayOrderView(hydOrder),
});
} else if (
intentStatus === DaimoPayIntentStatus.COMPLETED ||
intentStatus === DaimoPayIntentStatus.BOUNCED
) {
const event = {
type: intentStatus,
paymentId: writeDaimoPayOrderID(hydOrder.id),
chainId: hydOrder.destFinalCallTokenAmount.token.chainId,
txHash: assertNotNull(
hydOrder.destFastFinishTxHash ?? hydOrder.destClaimTxHash,
`[PAY BUTTON] dest tx hash null on order ${hydOrder.id} when intent status is ${intentStatus}`,
),
payment: getDaimoPayOrderView(hydOrder),
};

if (intentStatus === DaimoPayIntentStatus.COMPLETED) {
onPaymentCompleted?.(event as PaymentCompletedEvent);
} else if (intentStatus === DaimoPayIntentStatus.BOUNCED) {
onPaymentBounced?.(event as PaymentBouncedEvent);
}
}
}, [hydOrder?.intentStatus]);
}, [hydOrder?.id, intentStatus]);

useEffect(() => {
if (props.defaultOpen) {
Expand Down
25 changes: 20 additions & 5 deletions packages/connectkit/src/components/DaimoPayModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ export const DaimoPayModal: React.FC<{
} else if (context.route === ROUTES.WAITING_EXTERNAL) {
setPaymentWaitingMessage(undefined);
if (isDepositFlow) {
assert(payParams != null, "payParams cannot be null in deposit flow");
assert(
payParams != null,
"[PAY MODAL] payParams cannot be null in deposit flow",
);
generatePreviewOrder(payParams);
context.setRoute(ROUTES.SELECT_EXTERNAL_AMOUNT);
} else {
Expand All @@ -101,7 +104,10 @@ export const DaimoPayModal: React.FC<{
}
} else if (context.route === ROUTES.PAY_WITH_TOKEN) {
if (isDepositFlow) {
assert(payParams != null, "payParams cannot be null in deposit flow");
assert(
payParams != null,
"[PAY MODAL] payParams cannot be null in deposit flow",
);
generatePreviewOrder(payParams);
context.setRoute(ROUTES.SELECT_AMOUNT);
} else {
Expand All @@ -112,7 +118,10 @@ export const DaimoPayModal: React.FC<{
context.setRoute(ROUTES.CONNECTORS);
} else if (context.route === ROUTES.WAITING_DEPOSIT_ADDRESS) {
if (isDepositFlow) {
assert(payParams != null, "payParams cannot be null in deposit flow");
assert(
payParams != null,
"[PAY MODAL] payParams cannot be null in deposit flow",
);
generatePreviewOrder(payParams);
context.setRoute(ROUTES.SELECT_DEPOSIT_ADDRESS_AMOUNT);
} else {
Expand All @@ -124,7 +133,10 @@ export const DaimoPayModal: React.FC<{
context.setRoute(ROUTES.SOLANA_SELECT_TOKEN);
} else if (context.route === ROUTES.SOLANA_PAY_WITH_TOKEN) {
if (isDepositFlow) {
assert(payParams != null, "payParams cannot be null in deposit flow");
assert(
payParams != null,
"[PAY MODAL] payParams cannot be null in deposit flow",
);
generatePreviewOrder(payParams);
context.setRoute(ROUTES.SOLANA_SELECT_AMOUNT);
} else {
Expand Down Expand Up @@ -165,7 +177,10 @@ export const DaimoPayModal: React.FC<{

function hide() {
if (isDepositFlow) {
assert(payParams != null, "payParams cannot be null in deposit flow");
assert(
payParams != null,
"[PAY MODAL] payParams cannot be null in deposit flow",
);
generatePreviewOrder(payParams);
}
context.setOpen(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const Confirmation: React.FC = () => {
const txHash =
daimoPayOrder.destFastFinishTxHash ?? daimoPayOrder.destClaimTxHash;
const chainId = daimoPayOrder.destFinalCallTokenAmount.token.chainId;
assert(txHash != null, `Dest ${destStatus}, but missing txHash`);
assert(txHash != null, `[CONFIRMATION] dest status: ${destStatus}, but missing txHash`);
const txURL = getChainExplorerTxUrl(chainId, txHash);

paymentState.onSuccess({ txHash, txURL });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import {
} from "../../../Common/Modal/styles";

import { assert } from "@daimo/common";
import { motion } from "framer-motion";
import { css } from "styled-components";
import styled from "../../../../styles/styled";
import Button from "../../../Common/Button";
import PaymentBreakdown from "../../../Common/PaymentBreakdown";
import TokenLogoSpinner from "../../../Spinners/TokenLogoSpinner";
Expand All @@ -41,7 +38,10 @@ const PayWithSolanaToken: React.FC = () => {
const handleTransfer = async () => {
try {
setPayState(PayState.RequestingPayment);
assert(!!selectedSolanaTokenOption, "No token option selected");
assert(
!!selectedSolanaTokenOption,
"[PAY SOLANA] No token option selected",
);
await payWithSolanaToken(selectedSolanaTokenOption.required.token.token);

setPayState(PayState.RequestSuccessful);
Expand Down Expand Up @@ -92,7 +92,7 @@ const PayWithSolanaToken: React.FC = () => {
onClick={() => {
assert(
payParams != null,
"payParams cannot be null in deposit flow",
"[PAY SOLANA] payParams cannot be null in deposit flow",
);
generatePreviewOrder(payParams);
setRoute(ROUTES.SELECT_METHOD);
Expand All @@ -106,34 +106,4 @@ const PayWithSolanaToken: React.FC = () => {
);
};

const LoadingContainer = styled(motion.div)`
display: flex;
align-items: center;
justify-content: center;
margin: 10px auto 16px;
height: 120px;
`;
const AnimationContainer = styled(motion.div)<{
$circle: boolean;
}>`
user-select: none;
position: relative;
--spinner-error-opacity: 0;
&:before {
content: "";
position: absolute;
inset: 1px;
opacity: 0;
background: var(--ck-body-color-danger);
${(props) =>
props.$circle &&
css`
inset: -5px;
border-radius: 50%;
background: none;
box-shadow: inset 0 0 0 3.5px var(--ck-body-color-danger);
`}
}
`;

export default PayWithSolanaToken;
27 changes: 4 additions & 23 deletions packages/connectkit/src/hooks/useDaimoPayStatus.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,24 @@
import {
DaimoPayIntentStatus,
DaimoPayOrderMode,
DaimoPayOrderStatusSource,
writeDaimoPayOrderID,
} from "@daimo/common";
import { DaimoPayIntentStatus, writeDaimoPayOrderID } from "@daimo/common";
import { usePayContext } from "../components/DaimoPay";
import { PaymentStatus } from "../types";

/** Returns the current payment, or undefined if there is none.
*
* Status values:
* - `payment_pending` - the user has not paid yet
* - `payment_unpaid` - the user has not paid yet
* - `payment_started` - the user has paid & payment is in progress. This status
* typically lasts a few seconds.
* - `payment_completed` - the final call or transfer succeeded
* - `payment_bounced` - the final call or transfer reverted. Funds were sent
* to the payment's configured refund address on the destination chain.
*/
export function useDaimoPayStatus():
| { paymentId: string; status: PaymentStatus }
| { paymentId: string; status: DaimoPayIntentStatus }
| undefined {
const { paymentState } = usePayContext();
if (!paymentState || !paymentState.daimoPayOrder) return undefined;

const order = paymentState.daimoPayOrder;
const paymentId = writeDaimoPayOrderID(order.id);
if (order.mode === DaimoPayOrderMode.HYDRATED) {
if (order.intentStatus !== DaimoPayIntentStatus.PENDING) {
if (order.intentStatus === DaimoPayIntentStatus.SUCCESSFUL) {
return { paymentId, status: "payment_completed" };
} else {
return { paymentId, status: "payment_bounced" };
}
} else if (
order.sourceStatus !== DaimoPayOrderStatusSource.WAITING_PAYMENT
) {
return { paymentId, status: "payment_started" };
}
}

return { paymentId, status: "payment_pending" };
return { paymentId, status: order.intentStatus };
}
10 changes: 7 additions & 3 deletions packages/connectkit/src/hooks/usePayWithSolanaToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ export function usePayWithSolanaToken({
const wallet = useWallet();

const payWithSolanaToken = async (inputToken: SolanaPublicKey) => {
assert(!!wallet.publicKey, "No wallet connected");
assert(!!platform && !!daimoPayOrder);
assert(!!wallet.publicKey, "[PAY SOLANA] No wallet connected");
assert(!!daimoPayOrder, "[PAY SOLANA] daimoPayOrder cannot be null");
assert(!!platform, "[PAY SOLANA] platform cannot be null");

const orderId = daimoPayOrder.id;
const { hydratedOrder } = await createOrHydrate({
Expand All @@ -48,7 +49,10 @@ export function usePayWithSolanaToken({
try {
const serializedTx = await trpc.getSolanaSwapAndBurnTx.query({
orderId: orderId.toString(),
userPublicKey: assertNotNull(wallet.publicKey).toString(),
userPublicKey: assertNotNull(
wallet.publicKey,
"[PAY SOLANA] wallet.publicKey cannot be null",
).toString(),
inputTokenMint: inputToken,
});
const tx = VersionedTransaction.deserialize(hexToBytes(serializedTx));
Expand Down
8 changes: 6 additions & 2 deletions packages/connectkit/src/hooks/usePayWithToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export function usePayWithToken({

/** Commit to a token + amount = initiate payment. */
const payWithToken = async (tokenAmount: DaimoPayTokenAmount) => {
assert(!!daimoPayOrder && !!platform);
assert(!!daimoPayOrder, "[PAY TOKEN] daimoPayOrder cannot be null");
assert(!!platform, "[PAY TOKEN] platform cannot be null");

const { hydratedOrder } = await createOrHydrate({
order: daimoPayOrder,
Expand Down Expand Up @@ -74,7 +75,10 @@ export function usePayWithToken({
orderId: daimoPayOrder.id.toString(),
sourceInitiateTxHash: txHash,
sourceChainId: tokenAmount.token.chainId,
sourceFulfillerAddr: assertNotNull(senderAddr),
sourceFulfillerAddr: assertNotNull(
senderAddr,
`[PAY TOKEN] senderAddr cannot be null on order ${daimoPayOrder.id}`,
),
sourceToken: tokenAmount.token.token,
sourceAmount: tokenAmount.amount,
});
Expand Down
Loading

0 comments on commit 6ca9f5a

Please sign in to comment.