diff --git a/package-lock.json b/package-lock.json index f0938c2..2af6dd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,8 @@ "@polkadot/ui-settings": "^3.3.1", "@polkadot/util": "^12.1.2", "@polkadot/util-crypto": "^12.1.2", + "@stripe/react-stripe-js": "^3.7.0", + "@stripe/stripe-js": "^7.4.0", "@tanstack/react-query": "^5.65.1", "@types/libsodium-wrappers": "^0.7.14", "@types/react-simple-maps": "^3.0.6", @@ -5337,6 +5339,29 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@stripe/react-stripe-js": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.7.0.tgz", + "integrity": "sha512-PYls/2S9l0FF+2n0wHaEJsEU8x7CmBagiH7zYOsxbBlLIHEsqUIQ4MlIAbV9Zg6xwT8jlYdlRIyBTHmO3yM7kQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=1.44.1 <8.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.4.0.tgz", + "integrity": "sha512-lQHQPfXPTBeh0XFjq6PqSBAyR7umwcJbvJhXV77uGCUDD6ymXJU/f2164ydLMLCCceNuPlbV9b+1smx98efwWQ==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, "node_modules/@substrate/connect": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@substrate/connect/-/connect-0.8.10.tgz", diff --git a/package.json b/package.json index a15d28a..adbb7ca 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "@polkadot/ui-settings": "^3.3.1", "@polkadot/util": "^12.1.2", "@polkadot/util-crypto": "^12.1.2", + "@stripe/react-stripe-js": "^3.7.0", + "@stripe/stripe-js": "^7.4.0", "@tanstack/react-query": "^5.65.1", "@types/libsodium-wrappers": "^0.7.14", "@types/react-simple-maps": "^3.0.6", diff --git a/src/cyborg/components/accessCompute/modals/Payment.tsx b/src/cyborg/components/accessCompute/modals/Payment.tsx index 10f068b..f211796 100644 --- a/src/cyborg/components/accessCompute/modals/Payment.tsx +++ b/src/cyborg/components/accessCompute/modals/Payment.tsx @@ -14,25 +14,70 @@ import { usePriceQuery } from '../../../api/parachain/usePriceQuery' import useTransaction from '../../../api/parachain/useTransaction' import { useUserComputeHoursQuery } from '../../../api/parachain/useUserSubscription' import { transformToNumber } from '../../../util/numberOperations' +import { loadStripe } from '@stripe/stripe-js' +import { useAuth0 } from '@auth0/auth0-react' +import { Elements } from '@stripe/react-stripe-js' +import StripePaymentForm from '../../general/StripePaymentForm' const PAYMENT_OPTIONS = [ { name: 'ENTT', icon: robo, isAvailable: true, testnet: true }, { name: 'Crypto', icon: crypto, isAvailable: false, testnet: false }, - { name: '', icon: fiat, isAvailable: false, testnet: false }, + { name: 'FIAT', icon: fiat, isAvailable: true, testnet: false }, ] +const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLIC_KEY) + interface Props { onCancel: () => void onConfirm: () => void setService: () => void } -const PaymentModal: React.FC = ({ - onCancel, - onConfirm, -}: Props) => { +const PaymentModal: React.FC = ({ onCancel, onConfirm }: Props) => { const { api, currentAccount } = useSubstrateState() + const [clientSecret, setClientSecret] = useState('') + const [processingPayment, setProcessingPayment] = useState(false) + const { getAccessTokenSilently } = useAuth0() + + const handleFiatPayment = async (paymentMethodId: string) => { + setProcessingPayment(true) + + try { + const token = await getAccessTokenSilently() + + const response = await fetch( + `${process.env.REACT_APP_GATEKEEPER_HTTPS_URL}/payment/process`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ + amount: hoursSelected * computeHourPrice * 100, // Convert to cents + currency: 'usd', + payment_method_id: paymentMethodId, + account_id: currentAccount.toString(), + }), + } + ) + + if (!response.ok) { + throw new Error('Payment failed') + } + + const paymentIntent = await response.json() + toast.success('Payment successful!') + refetch() + onConfirm() + } catch (error) { + toast.error(error.message || 'Payment failed') + } finally { + setProcessingPayment(false) + } + } + const { data: computeHourPrice, //isLoading: computeHourPriceIsLoading, @@ -41,10 +86,10 @@ const PaymentModal: React.FC = ({ const { data: userComputeHours, - refetch + refetch, //isLoading: userComputeHoursIsLoading, - //error: userComputeHoursError - } = useUserComputeHoursQuery(); + //error: userComputeHoursError + } = useUserComputeHoursQuery() const [selectedOption, setSelectedOption] = useState(PAYMENT_OPTIONS[0].name) const [termsAreAccepted, setTermsAreAccepted] = useState(false) @@ -70,15 +115,16 @@ const PaymentModal: React.FC = ({ } const setHoursSelected = (hours: string) => { - setHoursSelectedNumber( - transformToNumber(hours) - ) + setHoursSelectedNumber(transformToNumber(hours)) } const { handleTransaction, isLoading } = useTransaction(api) const submitTransaction = async () => { - const tx = userComputeHours > 0 ? api.tx.payment.addHours(hoursSelected) : api.tx.payment.subscribe(hoursSelected) + const tx = + userComputeHours > 0 + ? api.tx.payment.addHours(hoursSelected) + : api.tx.payment.subscribe(hoursSelected) await handleTransaction({ tx, account: currentAccount, @@ -104,7 +150,9 @@ const PaymentModal: React.FC = ({ >
-
{userComputeHours > 0 ? "Extend Subscription" : "Subscribe"}
+
+ {userComputeHours > 0 ? 'Extend Subscription' : 'Subscribe'} +
= ({ > Cancel - + {selectedOption === 'FIAT' ? ( + + + + ) : ( + + )}
diff --git a/src/cyborg/components/general/StripePaymentForm.tsx b/src/cyborg/components/general/StripePaymentForm.tsx new file mode 100644 index 0000000..9b84255 --- /dev/null +++ b/src/cyborg/components/general/StripePaymentForm.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; +import Button from './buttons/Button'; + +interface StripePaymentFormProps { + onSubmit: (paymentMethodId: string) => void; + processing: boolean; +} + +const StripePaymentForm: React.FC = ({ onSubmit, processing }) => { + const stripe = useStripe(); + const elements = useElements(); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!stripe || !elements) { + return; + } + + const cardElement = elements.getElement(CardElement); + + if (!cardElement) return; + + const { error, paymentMethod } = await stripe.createPaymentMethod({ + type: 'card', + card: cardElement, + }); + + if (error) { + console.error(error); + return; + } + + if (paymentMethod) { + onSubmit(paymentMethod.id); + } + }; + + return ( +
+
+ +
+ +
+ ); +}; + +export default StripePaymentForm; \ No newline at end of file diff --git a/src/cyborg/components/general/buttons/Button.tsx b/src/cyborg/components/general/buttons/Button.tsx index 99d9837..a4360e5 100644 --- a/src/cyborg/components/general/buttons/Button.tsx +++ b/src/cyborg/components/general/buttons/Button.tsx @@ -7,26 +7,26 @@ interface Props { children: ReactNode type: 'submit' | 'reset' | 'button' onClick: (e: React.MouseEvent) => void - additionalClasses?: String + additionalClasses?: string selectable: { isSelected: boolean } | false + disabled?: boolean } -//with ts, add exclusivity of either varation === "cancel" const Button: React.FC = ({ variation, children, type, onClick, - additionalClasses, + additionalClasses = '', selectable, + disabled = false }: Props) => { const [btnState, setBtnState] = useState('initial') - let className: String + let className: string let content: ReactNode - const returnButtonClass = variation => { - //Order is important here! Change with care + const returnButtonClass = (variation: string) => { if (selectable) { if (selectable.isSelected) { return `btn-${variation}-selected` @@ -44,6 +44,7 @@ const Button: React.FC = ({ if (btnState === 'btn-off') { return `btn-${variation}-off` } + return '' } const handleMouseLeave = () => { @@ -84,12 +85,13 @@ const Button: React.FC = ({ type={type} onMouseEnter={() => setBtnState('btn-on')} onMouseLeave={() => handleMouseLeave()} - onClick={e => onClick(e)} - className={`${className}`} + onClick={(e) => !disabled && onClick(e)} + className={`${className} ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`} + disabled={disabled} > {content} ) } -export default Button +export default Button \ No newline at end of file