* basic working payments

* Select funds amount and prettier funds button / dialog

* Add funds page and nav menu option

* Format funds amount. Use ghost button for back.

* Add mantic dollars description

* Improve styles of add funds page

* about styling

* change faq => about

* change default font to Courier

* header sign out menu item; remove user  card

* keep logo font

* fix header issue

* stripe webhook: handle repeat events

* Make add funds button a gradient

* add funds referer url

* Fix add funds page after merge

* Slight VisD tweaks

* Update add funds button position. Mantic => Manifold

* Remove Add funds menu option for now.

* Set up product ids and endpoint for stripe prod

* Swap back order in profile menu

Co-authored-by: mantikoros <sgrugett@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
This commit is contained in:
James Grugett 2022-01-07 16:56:14 -06:00 committed by GitHub
parent fccf52eb34
commit af6387bbf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 451 additions and 2 deletions

View File

@ -18,7 +18,8 @@
"firebase-admin": "10.0.0", "firebase-admin": "10.0.0",
"firebase-functions": "3.16.0", "firebase-functions": "3.16.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"mailgun-js": "0.22.0" "mailgun-js": "0.22.0",
"stripe": "8.194.0"
}, },
"devDependencies": { "devDependencies": {
"@types/mailgun-js": "0.22.12", "@types/mailgun-js": "0.22.12",

View File

@ -5,5 +5,6 @@ admin.initializeApp()
// export * from './keep-awake' // export * from './keep-awake'
export * from './place-bet' export * from './place-bet'
export * from './resolve-market' export * from './resolve-market'
export * from './stripe'
export * from './sell-bet' export * from './sell-bet'
export * from './create-contract' export * from './create-contract'

126
functions/src/stripe.ts Normal file
View File

@ -0,0 +1,126 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import Stripe from 'stripe'
import { payUser } from './resolve-market'
const stripe = new Stripe(functions.config().stripe.apikey, {
apiVersion: '2020-08-27',
typescript: true,
})
// manage at https://dashboard.stripe.com/test/products?active=true
const manticDollarStripePrice =
admin.instanceId().app.options.projectId === 'mantic-markets'
? {
500: 'price_1KFQXcGdoFKoCJW770gTNBrm',
1000: 'price_1KFQp1GdoFKoCJW7Iu0dsF65',
2500: 'price_1KFQqNGdoFKoCJW7SDvrSaEB',
10000: 'price_1KFQraGdoFKoCJW77I4XCwM3',
}
: {
500: 'price_1K8W10GdoFKoCJW7KWORLec1',
1000: 'price_1K8bC1GdoFKoCJW76k3g5MJk',
2500: 'price_1K8bDSGdoFKoCJW7avAwpV0e',
10000: 'price_1K8bEiGdoFKoCJW7Us4UkRHE',
}
export const createCheckoutSession = functions
.runWith({ minInstances: 1 })
.https.onRequest(async (req, res) => {
const userId = req.query.userId?.toString()
const manticDollarQuantity = req.query.manticDollarQuantity?.toString()
if (!userId) {
res.status(400).send('Invalid user ID')
return
}
if (
!manticDollarQuantity ||
!Object.keys(manticDollarStripePrice).includes(manticDollarQuantity)
) {
res.status(400).send('Invalid Mantic Dollar quantity')
return
}
const referrer =
req.query.referer || req.headers.referer || 'https://mantic.markets'
const session = await stripe.checkout.sessions.create({
metadata: {
userId,
manticDollarQuantity,
},
line_items: [
{
price:
manticDollarStripePrice[
manticDollarQuantity as unknown as keyof typeof manticDollarStripePrice
],
quantity: 1,
},
],
mode: 'payment',
success_url: `${referrer}?funding-success`,
cancel_url: `${referrer}?funding-failiure`,
})
res.redirect(303, session.url || '')
})
export const stripeWebhook = functions
.runWith({ minInstances: 1 })
.https.onRequest(async (req, res) => {
let event
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
req.headers['stripe-signature'] as string,
functions.config().stripe.webhooksecret
)
} catch (e: any) {
console.log(`Webhook Error: ${e.message}`)
res.status(400).send(`Webhook Error: ${e.message}`)
return
}
if (event.type === 'checkout.session.completed') {
const session = event.data.object as any
await issueMoneys(session)
}
res.status(200).send('success')
})
const issueMoneys = async (session: any) => {
const { id: sessionId } = session
const query = await firestore
.collection('stripe-transactions')
.where('sessionId', '==', sessionId)
.get()
if (!query.empty) {
console.log('session', sessionId, 'already processed')
return
}
const { userId, manticDollarQuantity } = session.metadata
const payout = Number.parseInt(manticDollarQuantity)
await firestore.collection('stripe-transactions').add({
userId,
manticDollarQuantity: payout, // save as number
sessionId,
session,
})
await payUser([userId, payout])
console.log('user', userId, 'paid M$', payout)
}
const firestore = admin.firestore()

View File

@ -314,7 +314,7 @@
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": "@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@>=8.1.0":
version "17.0.1" version "17.0.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.1.tgz#88d501e84b6185f6489ecee4ba9e8fcec7f29bb2" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.1.tgz#88d501e84b6185f6489ecee4ba9e8fcec7f29bb2"
integrity sha512-NXKvBVUzIbs6ylBwmOwHFkZS2EXCcjnqr8ZCRNaXBkHAf+3mn/rPcJxwrzuc6movh8fxQAsUUfYklJ/EG+hZqQ== integrity sha512-NXKvBVUzIbs6ylBwmOwHFkZS2EXCcjnqr8ZCRNaXBkHAf+3mn/rPcJxwrzuc6movh8fxQAsUUfYklJ/EG+hZqQ==
@ -464,6 +464,14 @@ bytes@3.1.1:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.1.tgz#3f018291cb4cbad9accb6e6970bca9c8889e879a"
integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg==
call-bind@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
dependencies:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
cliui@^7.0.2: cliui@^7.0.2:
version "7.0.4" version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
@ -895,6 +903,11 @@ fresh@0.5.2:
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
ftp@~0.3.10: ftp@~0.3.10:
version "0.3.10" version "0.3.10"
resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d"
@ -946,6 +959,15 @@ get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
dependencies:
function-bind "^1.1.1"
has "^1.0.3"
has-symbols "^1.0.1"
get-stream@^6.0.0: get-stream@^6.0.0:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
@ -1018,6 +1040,18 @@ gtoken@^5.0.4:
google-p12-pem "^3.0.3" google-p12-pem "^3.0.3"
jws "^4.0.0" jws "^4.0.0"
has-symbols@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
hash-stream-validation@^0.2.2: hash-stream-validation@^0.2.2:
version "0.2.4" version "0.2.4"
resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512"
@ -1428,6 +1462,11 @@ object-hash@^2.1.1:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5"
integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
object-inspect@^1.9.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
on-finished@~2.3.0: on-finished@~2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@ -1605,6 +1644,13 @@ qs@6.9.6:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee" resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee"
integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ== integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==
qs@^6.6.0:
version "6.10.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.2.tgz#c1431bea37fc5b24c5bdbafa20f16bdf2a4b9ffe"
integrity sha512-mSIdjzqznWgfd4pMii7sHtaYF8rx8861hBO80SraY5GT0XQibWZWJSid0avzHGkDIZLImux2S5mXO0Hfct2QCw==
dependencies:
side-channel "^1.0.4"
range-parser@~1.2.1: range-parser@~1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
@ -1729,6 +1775,15 @@ setprototypeof@1.2.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
dependencies:
call-bind "^1.0.0"
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
signal-exit@^3.0.2: signal-exit@^3.0.2:
version "3.0.6" version "3.0.6"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.6.tgz#24e630c4b0f03fea446a2bd299e62b4a6ca8d0af"
@ -1822,6 +1877,14 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
dependencies: dependencies:
ansi-regex "^5.0.1" ansi-regex "^5.0.1"
stripe@8.194.0:
version "8.194.0"
resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.194.0.tgz#67fc7a34260f95f9103834a1f0962d27c608cf73"
integrity sha512-iERByJUNA7sdkfQ3fD1jcrAZqPxCtTmL2EUzvHUVLXyoacDrflkq4ux5KFxYhfCIerrOAhquVj17+sBHn96/Kg==
dependencies:
"@types/node" ">=8.1.0"
qs "^6.6.0"
stubs@^3.0.0: stubs@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b"

View File

@ -0,0 +1,76 @@
import clsx from 'clsx'
import { useState } from 'react'
import { useUser } from '../hooks/use-user'
import { checkoutURL } from '../lib/service/stripe'
import { FundsSelector } from './yes-no-selector'
export function AddFundsButton(props: { className?: string }) {
const { className } = props
const user = useUser()
const [amountSelected, setAmountSelected] = useState<
500 | 1000 | 2500 | 10000
>(500)
return (
<>
<label
htmlFor="add-funds"
className={clsx(
'btn btn-sm normal-case modal-button bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600 font-normal border-none',
className
)}
>
Add funds
</label>
<input type="checkbox" id="add-funds" className="modal-toggle" />
<div className="modal">
<div className="modal-box">
<div className="text-xl mb-6">Get Manifold Dollars</div>
<div className="text-gray-500 mb-6">
Use Manifold Dollars to trade in your favorite markets. <br /> (Not
redeemable for cash.)
</div>
<div className="text-gray-500 text-sm mb-2">Amount</div>
<FundsSelector
selected={amountSelected}
onSelect={setAmountSelected}
/>
<div className="mt-6">
<div className="text-gray-500 text-sm mb-1">Price USD</div>
<div className="text-xl">
${Math.round(amountSelected / 100)}.00
</div>
</div>
<div className="modal-action">
<label htmlFor="add-funds" className={clsx('btn btn-ghost')}>
Back
</label>
<form
action={checkoutURL(
user?.id || '',
amountSelected,
window.location.href
)}
method="POST"
>
<button
type="submit"
className="btn btn-primary px-10 font-medium bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
>
Checkout
</button>
</form>
</div>
</div>
</div>
</>
)
}

View File

@ -21,6 +21,7 @@ import {
calculatePayoutAfterCorrectBet, calculatePayoutAfterCorrectBet,
} from '../lib/calculate' } from '../lib/calculate'
import { firebaseLogin } from '../lib/firebase/users' import { firebaseLogin } from '../lib/firebase/users'
import { AddFundsButton } from './add-funds-button'
import { OutcomeLabel } from './outcome-label' import { OutcomeLabel } from './outcome-label'
import { AdvancedPanel } from './advanced-panel' import { AdvancedPanel } from './advanced-panel'
import { Bet } from '../lib/firebase/bets' import { Bet } from '../lib/firebase/bets'
@ -149,6 +150,7 @@ export function BetPanel(props: { contract: Contract; className?: string }) {
{error} {error}
</div> </div>
)} )}
{user && <AddFundsButton className="self-end mt-3" />}
</Col> </Col>
<div className="mt-2 mb-1 text-sm text-gray-400">Implied probability</div> <div className="mt-2 mb-1 text-sm text-gray-400">Implied probability</div>

View File

@ -48,6 +48,10 @@ function getNavigationOptions(user: User, options: { mobile: boolean }) {
name: 'Your markets', name: 'Your markets',
href: `/${user.username}`, href: `/${user.username}`,
}, },
// {
// name: 'Add funds',
// href: '/add-funds',
// },
{ {
name: 'Discord', name: 'Discord',
href: 'https://discord.gg/eHQBNBqXuh', href: 'https://discord.gg/eHQBNBqXuh',

View File

@ -1,5 +1,6 @@
import clsx from 'clsx' import clsx from 'clsx'
import React from 'react' import React from 'react'
import { formatMoney } from '../lib/util/format'
import { Col } from './layout/col' import { Col } from './layout/col'
import { Row } from './layout/row' import { Row } from './layout/row'
@ -80,6 +81,33 @@ export function YesNoCancelSelector(props: {
) )
} }
const fundAmounts = [500, 1000, 2500, 10000]
export function FundsSelector(props: {
selected: 500 | 1000 | 2500 | 10000
onSelect: (selected: 500 | 1000 | 2500 | 10000) => void
className?: string
btnClassName?: string
}) {
const { selected, onSelect, className } = props
const btnClassName = clsx('px-2 whitespace-nowrap', props.btnClassName)
return (
<Row className={clsx('space-x-3', className)}>
{fundAmounts.map((amount) => (
<Button
key={amount}
color={selected === amount ? 'green' : 'gray'}
onClick={() => onSelect(amount as any)}
className={btnClassName}
>
{formatMoney(amount)}
</Button>
))}
</Row>
)
}
function Button(props: { function Button(props: {
className?: string className?: string
onClick?: () => void onClick?: () => void

13
web/lib/service/stripe.ts Normal file
View File

@ -0,0 +1,13 @@
import { isProd } from '../firebase/init'
export const checkoutURL = (
userId: string,
manticDollarQuantity: number,
referer = ''
) => {
const endpoint = isProd
? 'https://us-central1-mantic-markets.cloudfunctions.net/createCheckoutSession'
: 'https://us-central1-dev-mantic-markets.cloudfunctions.net/createCheckoutSession'
return `${endpoint}?userId=${userId}&manticDollarQuantity=${manticDollarQuantity}&referer=${referer}`
}

68
web/pages/add-funds.tsx Normal file
View File

@ -0,0 +1,68 @@
import { useState } from 'react'
import { Col } from '../components/layout/col'
import { SEO } from '../components/SEO'
import { Title } from '../components/title'
import { FundsSelector } from '../components/yes-no-selector'
import { useUser } from '../hooks/use-user'
import { checkoutURL } from '../lib/service/stripe'
import Image from 'next/image'
import { Spacer } from '../components/layout/spacer'
import { Page } from '../components/page'
export default function AddFundsPage() {
const user = useUser()
const [amountSelected, setAmountSelected] = useState<
500 | 1000 | 2500 | 10000
>(500)
return (
<Page>
<SEO title="Add funds" description="Add funds" url="/add-funds" />
<Col className="items-center">
<Col>
<Title text="Get Manifold Dollars" />
<Image
className="block mt-6"
src="/praying-mantis-light.svg"
width={200}
height={200}
/>
<div className="text-gray-500 mb-6">
Use Manifold Dollars to trade in your favorite markets. <br /> (Not
redeemable for cash.)
</div>
<div className="text-gray-500 text-sm mb-2">Amount</div>
<FundsSelector
className="max-w-md"
selected={amountSelected}
onSelect={setAmountSelected}
/>
<div className="mt-6">
<div className="text-gray-500 text-sm mb-1">Price USD</div>
<div className="text-xl">
${Math.round(amountSelected / 100)}.00
</div>
</div>
<form
action={checkoutURL(user?.id || '', amountSelected)}
method="POST"
className="mt-12"
>
<button
type="submit"
className="btn btn-primary w-full font-medium bg-gradient-to-r from-teal-500 to-green-500 hover:from-teal-600 hover:to-green-600"
>
Checkout
</button>
</form>
</Col>
</Col>
</Page>
)
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB