Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/breezy-mirrors-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@crossmint/client-sdk-react-native-ui": minor
"@crossmint/client-sdk-react-ui": minor
"@crossmint/wallets-sdk": minor
---

Unifies callback calls for otp signers
5 changes: 1 addition & 4 deletions apps/wallets/quickstart-devkit/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,7 @@ function SolanaFirebaseProvider({ children }: { children: React.ReactNode }) {
function StellarCrossmintAuthProvider({ children }: { children: React.ReactNode }) {
return (
<CrossmintProvider apiKey={process.env.NEXT_PUBLIC_CROSSMINT_API_KEY || ""}>
<CrossmintAuthProvider
authModalTitle="Stellar Wallets Quickstart"
loginMethods={["google", "twitter", "email", "web3"]}
>
<CrossmintAuthProvider authModalTitle="Stellar Wallets Quickstart" loginMethods={["google", "email"]}>
<CrossmintWalletProvider
showPasskeyHelpers={false}
createOnLogin={{
Expand Down
15 changes: 13 additions & 2 deletions apps/wallets/quickstart-devkit/components/transfer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState } from "react";
import { useWallet } from "@crossmint/client-sdk-react-ui";
import { StellarWallet, useWallet } from "@crossmint/client-sdk-react-ui";
import { PublicKey } from "@solana/web3.js";
import { isAddress } from "viem";

Expand Down Expand Up @@ -271,7 +271,18 @@ export function StellarTransferFunds() {

try {
setIsLoading(true);
const tx = await wallet.send(recipient, token, amount.toString());
const transaction = {
type: "contract-call",
contractId: "CDMUQY2ZHWIGXSTHPHX53ACR76OLXCCAAKQWQWUS2JNAH6SZEXMRU6R2",
method: "transfer",
args: {
from: wallet.address,
to: recipient,
amount: amount,
},
};
const stellarWallet = StellarWallet.from(wallet);
const tx = await stellarWallet.sendTransaction(transaction);
setTxLink(tx.explorerLink);
} catch (err) {
console.error("Transfer: ", err);
Expand Down
24 changes: 19 additions & 5 deletions apps/wallets/smart-wallet/expo/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
useCrossmintAuth,
useWallet,
useWalletEmailSigner,
useWalletOtpSigner,
useCrossmint,
type Balances,
StellarWallet,
} from "@crossmint/client-sdk-react-native-ui";
import { useEffect, useMemo, useState } from "react";
import { ActivityIndicator, Button, Text, View, TextInput, StyleSheet, ScrollView, Alert } from "react-native";
Expand All @@ -15,7 +16,7 @@ export default function Index() {
const { experimental_customAuth } = useCrossmint();
const loggedInUserEmail = experimental_customAuth?.email ?? null;
const { wallet, getOrCreateWallet, status: walletStatus } = useWallet();
const { needsAuth, sendEmailWithOtp, verifyOtp } = useWalletEmailSigner();
const { needsAuth, sendOtp, verifyOtp } = useWalletOtpSigner();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needsAuth -- dont use Auth, name it something like isInitialized (and invert it)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

walletSignerNeedsSetup
otpSignerNeedsSetup
otpSignerInitialized

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be wrong but iirc its an anti pattern to have read and write properties in the same hook

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where does that come from? The most used hook in react has both a read and write state:

const [count, setCount] = useState(0);

const walletAddress = useMemo(() => wallet?.address, [wallet]);
const url = Linking.useURL();

Expand Down Expand Up @@ -80,7 +81,8 @@ export default function Index() {
}
setIsLoading(true);
try {
await getOrCreateWallet({ chain: "stellar", signer: { type: "email" } });
// await getOrCreateWallet({ chain: "stellar", signer: { type: "email" } });
await getOrCreateWallet({ chain: "stellar", signer: { type: "phone", phone: "+13472008361" } });
} catch (error) {
console.error("Error initializing wallet:", error);
} finally {
Expand Down Expand Up @@ -113,7 +115,7 @@ export default function Index() {
return;
}
setIsInOtpFlow(true);
await handleAction(sendEmailWithOtp);
await handleAction(sendOtp);
};

const handleVerifyOtpInput = async () => {
Expand All @@ -135,7 +137,19 @@ export default function Index() {
}
setIsLoading(true);
try {
const tx = await wallet.send(recipientAddress, "usdc", amount);
const transaction = {
type: "contract-call",
contractId: "CDMUQY2ZHWIGXSTHPHX53ACR76OLXCCAAKQWQWUS2JNAH6SZEXMRU6R2",
method: "transfer",
args: {
from: wallet.address,
to: recipientAddress,
amount,
},
};
const stellarWallet = StellarWallet.from(wallet);
const tx = await stellarWallet.sendTransaction(transaction);
// const tx = await wallet.send(recipientAddress, "usdc", amount);
console.log(`Sent ${amount} USDC to ${recipientAddress}. Tx Link: ${tx.explorerLink}`);
setTxLink(tx.explorerLink);
setRecipientAddress("");
Expand Down
2 changes: 1 addition & 1 deletion packages/client/ui/react-native/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { useCrossmint, useWallet } from "@crossmint/client-sdk-react-base";
export * from "./useCrossmintAuth";
export * from "./useWalletEmailSigner";
export * from "./useWalletOtpSigner";
28 changes: 0 additions & 28 deletions packages/client/ui/react-native/src/hooks/useWalletEmailSigner.ts

This file was deleted.

28 changes: 28 additions & 0 deletions packages/client/ui/react-native/src/hooks/useWalletOtpSigner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { CrossmintWalletEmailSignerContext } from "@/providers/CrossmintWalletProvider";
import { useContext } from "react";

export type OtpSignerFunctions = {
needsAuth: boolean;
sendOtp: () => Promise<void>;
verifyOtp: (otp: string) => Promise<void>;
reject: (error: Error) => void;
};

export function useWalletOtpSigner(): OtpSignerFunctions {
const context = useContext(CrossmintWalletEmailSignerContext);

if (context == null) {
throw new Error("useWalletOtpSigner must be used within CrossmintWalletProvider");
}

if (!context.sendOtp || !context.verifyOtp || !context.reject) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - re these nullable checks? If so, can you pls use context.sendOtp == null etc to be explicit (Crossmint code convention)

throw new Error("Otp signer functions are not available. Make sure you're using an otp signer wallet.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error seems a bit hard to parse, do we call these "otp signer wallet" when you create a wallet?

Doesnt a wallet have multple signers? If so does only one of them need to have OTP?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somehting like "The wallet in context doesn't have any OTP signer configured. Register an OTP signer first before initializing this hook."

}

return {
needsAuth: context.needsAuth,
sendOtp: context.sendOtp,
verifyOtp: context.verifyOtp,
reject: context.reject,
};
}
1 change: 1 addition & 0 deletions packages/client/ui/react-native/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export {
type Transaction,
EVMWallet,
SolanaWallet,
StellarWallet,
Wallet,
} from "@crossmint/wallets-sdk";
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ const throwNotAvailable = (functionName: string) => () => {

type CrossmintWalletEmailSignerContext = {
needsAuth: boolean;
sendEmailWithOtp: () => Promise<void>;
sendOtp: () => Promise<void>;
verifyOtp: (otp: string) => Promise<void>;
reject: (error?: Error) => void;
};

// Create the auth context
export const CrossmintWalletEmailSignerContext = createContext<CrossmintWalletEmailSignerContext>({
needsAuth: false,
sendEmailWithOtp: throwNotAvailable("sendEmailWithOtp"),
sendOtp: throwNotAvailable("sendOtp"),
verifyOtp: throwNotAvailable("verifyOtp"),
reject: throwNotAvailable("reject"),
});
Expand Down Expand Up @@ -49,7 +49,7 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }:
null
);
const [needsAuthState, setNeedsAuthState] = useState<boolean>(false);
const sendEmailWithOtpRef = useRef<() => Promise<void>>(throwNotAvailable("sendEmailWithOtp"));
const sendOtpRef = useRef<() => Promise<void>>(throwNotAvailable("sendOtp"));
const verifyOtpRef = useRef<(otp: string) => Promise<void>>(throwNotAvailable("verifyOtp"));
const rejectRef = useRef<(error?: Error) => void>(throwNotAvailable("reject"));

Expand Down Expand Up @@ -139,20 +139,20 @@ export function CrossmintWalletProvider({ children, createOnLogin, callbacks }:

const onAuthRequired = async (
needsAuth: boolean,
sendEmailWithOtp: () => Promise<void>,
sendOtp: () => Promise<void>,
verifyOtp: (otp: string) => Promise<void>,
reject: () => void
) => {
setNeedsAuthState(needsAuth);
sendEmailWithOtpRef.current = sendEmailWithOtp;
sendOtpRef.current = sendOtp;
verifyOtpRef.current = verifyOtp;
rejectRef.current = reject;
};

const authContextValue = useMemo(
() => ({
needsAuth: needsAuthState,
sendEmailWithOtp: sendEmailWithOtpRef.current,
sendOtp: sendOtpRef.current,
verifyOtp: verifyOtpRef.current,
reject: rejectRef.current,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function CrossmintWalletProvider({
const [needsAuthState, setNeedsAuthState] = useState<boolean>(false);

// Email signer refs (for main wallet authentication)
const sendEmailWithOtpRef = useRef<() => Promise<void>>(throwNotAvailable("sendEmailWithOtp"));
const sendOtpRef = useRef<() => Promise<void>>(throwNotAvailable("sendOtp"));
const verifyOtpRef = useRef<(otp: string) => Promise<void>>(throwNotAvailable("verifyOtp"));

// Phone signer refs (for TEE handshake)
Expand Down Expand Up @@ -99,7 +99,7 @@ export function CrossmintWalletProvider({

const emailsigners_handleSendEmailOTP = async () => {
try {
await sendEmailWithOtpRef.current();
await sendOtpRef.current();
setEmailSignerDialogStep("otp");
} catch (error) {
console.error("Failed to send email OTP", error);
Expand Down Expand Up @@ -166,7 +166,7 @@ export function CrossmintWalletProvider({
} else {
// Default email signer behavior
setEmailSignerDialogOpen(needsAuth);
sendEmailWithOtpRef.current = sendMessageWithOtp;
sendOtpRef.current = sendMessageWithOtp;
verifyOtpRef.current = verifyOtp;
}
setNeedsAuthState(needsAuth);
Expand Down
10 changes: 5 additions & 5 deletions packages/wallets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A Typescript SDK to interact with Crossmint Wallets. This SDK enables developers to easily create and manage wallets on Solana and EVM chains.

Get a Crossmint client API key from [here](https://docs.crossmint.com/introduction/platform/api-keys/client-side) and add it to your `.env` file. Make sure your API key has all scopes for `Wallet API`, and `Users`.
Get a Crossmint client API key from [here](https://docs.crossmint.com/introduction/platform/api-keys/client-side) and add it to your `.env` file. Make sure your API key has all scopes for `Wallet API`, and `Users`.

## Installation

Expand All @@ -23,18 +23,18 @@ const crossmint = createCrossmint({
});
const crossmintWallets = CrossmintWallets.from(crossmint);
const wallet = await crossmintWallets.getOrCreateWallet({
chain: "<your-chain>",
chain: "<your-chain>",
signer: {
type: "email",
email: "<your-email>",
onAuthRequired: async (needsAuth, sendEmailWithOtp, verifyOtp, reject) => {
onAuthRequired: async (needsAuth, sendOtp, verifyOtp, reject) => {
if (needsAuth) {
await sendEmailWithOtp();
await sendOtp();
// Prompt the user to check their email for the OTP code.
// Once the user provides the OTP, pass it to verifyOtp(otp).
// NOTE: If using our React/React Native SDK, this is handled automatically by the provider.
}
},
},
},
});

Expand Down
2 changes: 1 addition & 1 deletion packages/wallets/src/signers/non-custodial/ncs-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export abstract class NonCustodialSigner implements Signer {
`${this.type} signer requires the onAuthRequired callback to handle OTP verification. ` +
`This callback manages the authentication flow (sending OTP and verifying user input). ` +
`If using our React/React Native SDK, this is handled automatically by the provider. ` +
`For other environments, implement: onAuthRequired: (needsAuth, sendEmailWithOtp, verifyOtp, reject) => { /* your UI logic */ }`
`For other environments, implement: onAuthRequired: (needsAuth, sendOtp, verifyOtp, reject) => { /* your UI logic */ }`
);
}
throw new Error("Handshake parent not initialized");
Expand Down
4 changes: 2 additions & 2 deletions packages/wallets/src/signers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type EmailSignerConfig = {
email?: string;
onAuthRequired?: (
needsAuth: boolean,
sendEmailWithOtp: () => Promise<void>,
sendOtp: () => Promise<void>,
verifyOtp: (otp: string) => Promise<void>,
reject: () => void
) => Promise<void>;
Expand All @@ -42,7 +42,7 @@ export type PhoneSignerConfig = {
phone?: string;
onAuthRequired?: (
needsAuth: boolean,
sendEmailWithOtp: () => Promise<void>,
sendOtp: () => Promise<void>,
verifyOtp: (otp: string) => Promise<void>,
reject: () => void
) => Promise<void>;
Expand Down