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
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
107 changes: 82 additions & 25 deletions src/cyborg/components/accessCompute/modals/Payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = ({
onCancel,
onConfirm,
}: Props) => {
const PaymentModal: React.FC<Props> = ({ 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,
Expand All @@ -41,10 +86,10 @@ const PaymentModal: React.FC<Props> = ({

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)
Expand All @@ -70,15 +115,16 @@ const PaymentModal: React.FC<Props> = ({
}

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,
Expand All @@ -104,7 +150,9 @@ const PaymentModal: React.FC<Props> = ({
>
<div className="flex flex-col gap-6 w-full h-full rounded-lg text-lg">
<div className="flex justify-between">
<div className="text-2xl font-bold">{userComputeHours > 0 ? "Extend Subscription" : "Subscribe"}</div>
<div className="text-2xl font-bold">
{userComputeHours > 0 ? 'Extend Subscription' : 'Subscribe'}
</div>
<CloseButton
type="button"
onClick={onCancel}
Expand Down Expand Up @@ -193,18 +241,27 @@ const PaymentModal: React.FC<Props> = ({
>
Cancel
</Button>
<Button
type="button"
selectable={false}
variation="primary"
additionalClasses="w-full"
onClick={startTransaction}
>
<div className="flex gap-2 justify-center">
<div>Confirm Payment and Proceed to Upload</div>
<TiArrowRight />
</div>
</Button>
{selectedOption === 'FIAT' ? (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<StripePaymentForm
onSubmit={handleFiatPayment}
processing={processingPayment}
/>
</Elements>
) : (
<Button
type="button"
selectable={false}
variation="primary"
additionalClasses="w-full"
onClick={startTransaction}
>
<div className="flex gap-2 justify-center">
<div>Confirm Payment and Proceed to Upload</div>
<TiArrowRight />
</div>
</Button>
)}
</div>
</div>
</Modal>
Expand Down
72 changes: 72 additions & 0 deletions src/cyborg/components/general/StripePaymentForm.tsx
Original file line number Diff line number Diff line change
@@ -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<StripePaymentFormProps> = ({ 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 (
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<div className="border border-gray-600 p-3 rounded-lg">
<CardElement options={{
style: {
base: {
fontSize: '16px',
color: '#ffffff',
'::placeholder': {
color: '#aab7c4',
},
},
invalid: {
color: '#fa755a',
},
},
}} />
</div>
<Button
type="submit"
variation="primary"
selectable={false}
additionalClasses="w-full"
disabled={!stripe || processing}
onClick={() => {}}
>
{processing ? 'Processing...' : 'Pay with Credit Card'}
</Button>
</form>
);
};

export default StripePaymentForm;
20 changes: 11 additions & 9 deletions src/cyborg/components/general/buttons/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props> = ({
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`
Expand All @@ -44,6 +44,7 @@ const Button: React.FC<Props> = ({
if (btnState === 'btn-off') {
return `btn-${variation}-off`
}
return ''
}

const handleMouseLeave = () => {
Expand Down Expand Up @@ -84,12 +85,13 @@ const Button: React.FC<Props> = ({
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}
</button>
)
}

export default Button
export default Button