diff --git a/apps/platform/package.json b/apps/platform/package.json index 2cbec09..3e1eebf 100644 --- a/apps/platform/package.json +++ b/apps/platform/package.json @@ -15,6 +15,8 @@ "@muqa/db": "workspace:*", "@next-auth/prisma-adapter": "^1.0.7", "@react-google-maps/api": "^2.19.3", + "@stripe/crypto": "^0.0.4", + "@stripe/stripe-js": "^4.5.0", "axios": "^1.7.4", "ethers": "^6.13.2", "next": "14.2.2", @@ -22,6 +24,7 @@ "next-intl": "^3.17.2", "react": "18", "react-dom": "18", + "stripe": "^16.12.0", "wagmi": "2.9.0", "zod": "^3.23.8" }, diff --git a/apps/platform/src/app/api/create-onramp-session/route.ts b/apps/platform/src/app/api/create-onramp-session/route.ts new file mode 100644 index 0000000..3be9b90 --- /dev/null +++ b/apps/platform/src/app/api/create-onramp-session/route.ts @@ -0,0 +1,82 @@ +import Stripe from 'stripe'; +import { NextResponse } from 'next/server'; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', +}; + +const OnrampSessionResource = Stripe.StripeResource.extend({ + create: Stripe.StripeResource.method({ + method: 'POST', + path: 'crypto/onramp_sessions', + }), +}); + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', { + apiVersion: '2024-06-20', +}); + +export async function OPTIONS() { + return NextResponse.json({}, { headers: corsHeaders }); +} + +export async function POST(req: Request, { params }: { params: any }) { + const { transaction_details } = await req.json(); + + let clientSecret = ''; + + const apiKey = process.env.STRIPE_SECRET_KEY || ''; + const url = 'https://api.stripe.com/v1/crypto/onramp_sessions'; + + const requestData = new URLSearchParams(); + requestData.append('customer_ip_address', '8.8.8.8'); + // requestData.append( + // 'wallet_addresses[solana]', + // '0x495A28448A06B0DF634750EB062311dDC40B3ae5', + // ); + // requestData.append('destination_networks[]', 'solana'); + // requestData.append('destination_currencies[]', 'usdc'); + // requestData.append('destination_network', 'solana'); + // requestData.append('destination_currency', 'usdc'); + // requestData.append('destination_amount', '10'); + requestData.append( + 'wallet_addresses[ethereum]', + '0x495A28448A06B0DF634750EB062311dDC40B3ae5', + ); + requestData.append('destination_networks[]', 'ethereum'); + requestData.append('destination_currencies[]', 'usdc'); + requestData.append('destination_network', 'ethereum'); + requestData.append('destination_currency', 'usdc'); + requestData.append('destination_amount', '6'); + + const headers = { + Authorization: `Basic ${Buffer.from(apiKey + ':').toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + const requestOptions = { + method: 'POST', + headers: headers, + body: requestData, + }; + + await fetch(url, requestOptions) + .then(response => response.json()) + .then(data => { + clientSecret = data.client_secret; + }) + .catch(error => { + console.error('Error creating onramp session:', error); + }); + + return NextResponse.json( + { + clientSecret: clientSecret, + }, + { + headers: corsHeaders, + }, + ); +} diff --git a/apps/platform/src/app/stripe/StripeCryptoElements.tsx b/apps/platform/src/app/stripe/StripeCryptoElements.tsx new file mode 100644 index 0000000..193816c --- /dev/null +++ b/apps/platform/src/app/stripe/StripeCryptoElements.tsx @@ -0,0 +1,89 @@ +import React from 'react'; + +// ReactContext to simplify access of StripeOnramp object +const CryptoElementsContext = React.createContext(null); +CryptoElementsContext.displayName = 'CryptoElementsContext'; + +export const CryptoElements = ({ stripeOnramp, children }: any) => { + const [ctx, setContext] = React.useState(() => ({ + onramp: null, + })); + + React.useEffect(() => { + let isMounted = true; + + Promise.resolve(stripeOnramp).then((onramp) => { + if (onramp && isMounted) { + setContext((ctx) => (ctx.onramp ? ctx : { onramp })); + } + }); + + return () => { + isMounted = false; + }; + }, [stripeOnramp]); + + return ( + + {children} + + ); +}; + +// React hook to get StripeOnramp from context +export const useStripeOnramp = () => { + const context = React.useContext(CryptoElementsContext) as any; + return context?.onramp; +}; + +// React element to render Onramp UI +const useOnrampSessionListener = (type: any, session: any, callback: any) => { + React.useEffect(() => { + if (session && callback) { + const listener = (e: { payload: any; }) => callback(e.payload); + session.addEventListener(type, listener); + return () => { + session.removeEventListener(type, listener); + }; + } + return () => {}; + }, [session, callback, type]); +}; + +export const OnrampElement = ({ + clientSecret, + appearance, + onReady, + onChange, + ...props +}: any) => { + const stripeOnramp = useStripeOnramp(); + const onrampElementRef = React.useRef(null); + const [session, setSession] = React.useState(); + + const appearanceJSON = JSON.stringify(appearance); + React.useEffect(() => { + const containerRef = onrampElementRef.current as any; + if (containerRef) { + // NB: ideally we want to be able to hot swap/update onramp iframe + // This currently results a flash if one needs to mint a new session when they need to udpate fixed transaction details + containerRef.innerHTML = ''; + + if (clientSecret && stripeOnramp) { + setSession( + stripeOnramp + .createSession({ + clientSecret, + appearance: appearanceJSON ? JSON.parse(appearanceJSON) : {}, + }) + .mount(containerRef) + ); + } + } + }, [appearanceJSON, clientSecret, stripeOnramp]); + + useOnrampSessionListener('onramp_ui_loaded', session, onReady); + useOnrampSessionListener('onramp_session_updated', session, onChange); + + return
; +}; diff --git a/apps/platform/src/app/stripe/StripeCryptoElementsOLD.tsx b/apps/platform/src/app/stripe/StripeCryptoElementsOLD.tsx new file mode 100644 index 0000000..6f2376e --- /dev/null +++ b/apps/platform/src/app/stripe/StripeCryptoElementsOLD.tsx @@ -0,0 +1,77 @@ +'use client'; + +import React, { ReactNode } from 'react'; + +const CryptoElementsContext = React.createContext<{ onramp: any } | null>(null); + +interface CryptoElementsProps { + stripeOnramp: any; + children: ReactNode; +} + +export const CryptoElements: React.FC = ({ + stripeOnramp, + children, +}) => { + const [ctx, setContext] = React.useState(() => ({ onramp: null })); + + React.useEffect(() => { + let isMounted = true; + + Promise.resolve(stripeOnramp).then(onramp => { + if (onramp && isMounted) { + setContext(ctx => (ctx.onramp ? ctx : { onramp })); + } + }); + + return () => { + isMounted = false; + }; + }, [stripeOnramp]); + + return ( + + {children} + + ); +}; + +// React hook to get StripeOnramp from context +export const useStripeOnramp = () => { + const context = React.useContext(CryptoElementsContext); + return context?.onramp; +}; + +// React element to render Onramp UI +interface OnrampElementProps { + clientSecret: string; + appearance: any; // Replace 'any' with the appropriate type if known + [key: string]: any; +} + +export const OnrampElement: React.FC = ({ + clientSecret, + appearance, + ...props +}) => { + const stripeOnramp = useStripeOnramp(); + const onrampElementRef = React.useRef(null); + + React.useEffect(() => { + const containerRef = onrampElementRef.current; + if (containerRef) { + containerRef.innerHTML = ''; + + if (clientSecret && stripeOnramp) { + stripeOnramp + .createSession({ + clientSecret, + appearance, + }) + .mount(containerRef); + } + } + }, [clientSecret, stripeOnramp]); + + return
; +}; diff --git a/apps/platform/src/app/stripe/page.tsx b/apps/platform/src/app/stripe/page.tsx new file mode 100644 index 0000000..8bcb442 --- /dev/null +++ b/apps/platform/src/app/stripe/page.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { loadStripeOnramp } from '@stripe/crypto'; +import { + CryptoElements, + OnrampElement, +} from '@/app/stripe/StripeCryptoElements'; +import Container from '@/app/components/Container'; +import React, { useEffect, useState } from 'react'; + +const stripeOnrampPromise = loadStripeOnramp( + process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '', +); + +export default function Stripe() { + // const clientSecret = process.env.STRIPE_SECRET_KEY || ''; + + const [clientSecret, setClientSecret] = useState(''); + const [message, setMessage] = useState(''); + + useEffect(() => { + // Fetches an onramp session and captures the client secret + fetch(`/api/create-onramp-session`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + transaction_details: { + destination_currency: 'usdc', + destination_exchange_amount: '0.0001', + destination_network: 'gnosis', + }, + }), + }) + .then(res => res.json()) + .then(data => { + console.log('data', data); + setClientSecret(data.clientSecret); + }); + }, []); + + const onChange = React.useCallback(({ session }: any) => { + setMessage(`OnrampSession is now in ${session.status} state.`); + }, []); + + return ( +
+ +

+ STRIPE TEST +

+ + + +
+
+ ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e377e1..bf010a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,12 @@ importers: '@react-google-maps/api': specifier: ^2.19.3 version: 2.19.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@stripe/crypto': + specifier: ^0.0.4 + version: 0.0.4(@stripe/stripe-js@4.5.0) + '@stripe/stripe-js': + specifier: ^4.5.0 + version: 4.5.0 axios: specifier: ^1.7.4 version: 1.7.4 @@ -50,6 +56,9 @@ importers: react-dom: specifier: '18' version: 18.3.1(react@18.3.1) + stripe: + specifier: ^16.12.0 + version: 16.12.0 wagmi: specifier: 2.9.0 version: 2.9.0(@react-native-async-storage/async-storage@1.24.0(react-native@0.74.3(@babel/core@7.24.8)(@babel/preset-env@7.24.8(@babel/core@7.24.8))(@types/react@18.3.3)(bufferutil@4.0.8)(react@18.3.1)))(@tanstack/query-core@5.51.1)(@tanstack/react-query@5.51.1(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.3(@babel/core@7.24.8)(@babel/preset-env@7.24.8(@babel/core@7.24.8))(@types/react@18.3.3)(bufferutil@4.0.8)(react@18.3.1))(react@18.3.1)(rollup@4.18.1)(typescript@5.5.3)(viem@2.17.4(bufferutil@4.0.8)(typescript@5.5.3)(zod@3.23.8))(zod@3.23.8) @@ -2996,6 +3005,15 @@ packages: '@stablelib/x25519@1.0.3': resolution: {integrity: sha512-KnTbKmUhPhHavzobclVJQG5kuivH+qDLpe84iRqX3CLrKp881cF160JvXJ+hjn1aMyCwYOKeIZefIH/P5cJoRw==} + '@stripe/crypto@0.0.4': + resolution: {integrity: sha512-gcD/aG0N90ZrNVppWYf9ADPECptw6PVtF67VIeaFP7fhgd2NvNx8erkzlcvk3VIVSY+bZ6YGX7c7cASoySX74Q==} + peerDependencies: + '@stripe/stripe-js': ^1.46.0 + + '@stripe/stripe-js@4.5.0': + resolution: {integrity: sha512-dMOzc58AOlsF20nYM/avzV8RFhO/vgYTY7ajLMH6mjlnZysnOHZxsECQvjEmL8Q/ukPwHkOnxSPW/QGCCnp7XA==} + engines: {node: '>=12.16'} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -7045,6 +7063,10 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -7771,6 +7793,10 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + stripe@16.12.0: + resolution: {integrity: sha512-H7eFVLDxeTNNSn4JTRfL2//LzCbDrMSZ+2q1c7CanVWgK2qIW5TwS+0V7N9KcKZZNpYh/uCqK0PyZh/2UsaAtQ==} + engines: {node: '>=12.*'} + strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} @@ -12208,6 +12234,12 @@ snapshots: '@stablelib/random': 1.0.2 '@stablelib/wipe': 1.0.1 + '@stripe/crypto@0.0.4(@stripe/stripe-js@4.5.0)': + dependencies: + '@stripe/stripe-js': 4.5.0 + + '@stripe/stripe-js@4.5.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.12': @@ -17475,6 +17507,10 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 + qs@6.13.0: + dependencies: + side-channel: 1.0.6 + query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 @@ -18391,6 +18427,11 @@ snapshots: strip-json-comments@3.1.1: {} + stripe@16.12.0: + dependencies: + '@types/node': 20.14.10 + qs: 6.13.0 + strnum@1.0.5: {} style-to-object@1.0.6: