mirror of
https://github.com/enso-org/enso.git
synced 2024-12-27 22:18:43 +03:00
Add Stripe for Billing Support (#8841)
* feat: Stripe billing support Squashed commit of the following: commit b7ab361d2e2a3b11819ee0c964dd25dde2850eac Author: Nikita Pekin <nikita@frecency.com> Date: Wed Jan 10 04:14:14 2024 -0500 fixes commit 2b7f525be95d8d9e50dea9c5f31828dc2c823eae Merge: 717fba94a1942e6c2305
Author: Nikita Pekin <nikita@frecency.com> Date: Mon Jan 8 08:54:15 2024 -0500 Merge branch 'develop' into wip/np/payment-page-2 commit 717fba94a1b900318ae7d32664b1cb292cb47364 Author: Nikita Pekin <nikita@frecency.com> Date: Mon Jan 8 08:32:38 2024 -0500 fix commit 66a278effddfe57d326acfe93b9fd6ce9f849a65 Author: Nikita Pekin <nikita@frecency.com> Date: Mon Jan 8 06:10:37 2024 -0500 rename endpoints commit 05ca2276d796d5431a19623f18d97503d730746c Author: Nikita Pekin <nikita@frecency.com> Date: Thu Jan 4 03:13:07 2024 -0500 update for new API commit ecc65a4b3bbf8167c91eb9cc9a71f05367ee41f6 Author: Nikita Pekin <nikita@frecency.com> Date: Tue Jan 2 09:02:23 2024 -0500 make subscribe appear in app commit 048883e343cc42ba75e2e1ebbfa50b9d3033255c Author: Nikita Pekin <nikita@frecency.com> Date: Mon Jan 1 05:13:04 2024 -0500 unify pricename and price commit 5439299eaa01732bcee3204c72987845a569029b Author: Nikita Pekin <nikita@frecency.com> Date: Sun Dec 31 22:57:52 2023 -0500 rename checkout sessions endpoint commit 67537302f9183918272324723b34e26659d10dbe Author: Nikita Pekin <nikita@frecency.com> Date: Sun Dec 31 22:57:44 2023 -0500 fix session ID commit 637968331bf3d2c10b9c6130ae994529b9606fdd Author: Nikita Pekin <nikita@frecency.com> Date: Sun Dec 31 19:59:29 2023 -0500 fix stripe JS commit 051a01e1988f62931e2b7f3f436b6490a09602e0 Author: Nikita Pekin <nikita@frecency.com> Date: Sat Dec 30 23:32:41 2023 -0500 tmp: add AWS profile and refactor commit 9f4199b22dfc5565bea737e31f8d379e098712a7 Author: somebody1234 <ehern.lee@gmail.com> Date: Sat Nov 4 04:49:29 2023 +1000 Fix `ALL_PATHS_REGEX` commit 4b53bcf7f82fe30c21db013d01dae58e20afb605 Author: somebody1234 <ehern.lee@gmail.com> Date: Mon Dec 18 17:15:33 2023 +1000 Expose `unauthenticatedBackend` from backend context commit 8d554ac16747392c9cd5d10a2c3ad6d79afb7268 Author: somebody1234 <ehern.lee@gmail.com> Date: Mon Dec 18 17:12:17 2023 +1000 Add methods for making HTTP requests to unauthenticated backend commit 2010890cbd38bff31b18e0847ea22a5b71f926d1 Author: somebody1234 <ehern.lee@gmail.com> Date: Mon Dec 18 17:04:49 2023 +1000 Add unauthenticated backend commit 04ac84533bee493194e32129f934ccd9c1df78d6 Merge: 1fa45bc73cd4714af826
Author: somebody1234 <ehern.lee@gmail.com> Date: Mon Dec 18 16:26:27 2023 +1000 Merge branch 'develop' into wip/np/payment-page-2 commit 1fa45bc73cbbf50e53c6f3273559210e85b66c7e Author: Nikita Pekin <nikita@frecency.com> Date: Sun Nov 12 07:01:45 2023 +0000 tmp: Complete checkoutSession flow commit 30ec2792256db5b2b448119b07213b79e3f8a3c5 Author: somebody1234 <ehern.lee@gmail.com> Date: Wed Nov 1 19:20:15 2023 +1000 Initial Stripe integration * revert requestedPlan changes * switch to path from query * Prettier * Fix type error * Switch environment back to production * Fix errors * Fix dev server by removing COOP/COEP/CORP on the dev server specifically * Redirect after upgrading plan is successful * Fix errors; fix initial size of Subscribe page --------- Co-authored-by: somebody1234 <ehern.lee@gmail.com>
This commit is contained in:
parent
7f5b2edbf5
commit
5e4a7cf01c
@ -66,6 +66,37 @@ function rejectPermissionRequests() {
|
||||
})
|
||||
}
|
||||
|
||||
/** This Electron app is configured with extra CORS headers. Those headers are added because they
|
||||
* increase security and enable higher resolution for `performance.now()` timers. However, one of
|
||||
* these headers (i.e., `Cross-Origin-Embedder-Policy: require-corp`), breaks the Stripe.js library.
|
||||
* This is because Stripe.js does not provide a `Cross-Origin-Resource-Policy` header or
|
||||
* `Cross-Origin-Embedder-Policy` header on resources hosted at `https://js.stripe.com`. Without
|
||||
* these headers, the browser will not load the resources. To fix this without compromising security
|
||||
* or profiling capabilities, add the missing headers to the Stripe.js resources by intercepting the
|
||||
* response headers.
|
||||
*
|
||||
* At the time of writing, these are the Stripe.js resources in question:
|
||||
*
|
||||
* - https://js.stripe.com/v3/m-outer-3437aaddcdf6922d623e172c2d6f9278.html
|
||||
* - https://js.stripe.com/v3/fingerprinted/js/embedded-checkout-outer-d5c7fe9d44281b88cdffdf803de759f1.js
|
||||
* - https://js.stripe.com/v3/controller-a8f00e403bc9538a7c1880ae6b6a2dc3.html
|
||||
*
|
||||
* The missing headers are added more generally to all resources hosted at `https://js.stripe.com`.
|
||||
* This is because the resources are fingerprinted and the fingerprint changes every time the
|
||||
* resources are updated. Additionally, Stripe.js may choose to add more resources in the future. */
|
||||
function addMissingCorsHeaders() {
|
||||
void electron.app.whenReady().then(() => {
|
||||
electron.session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
details.responseHeaders = details.responseHeaders ?? {}
|
||||
if (details.url.includes('https://js.stripe.com/v3/')) {
|
||||
details.responseHeaders['Cross-Origin-Resource-Policy'] = ['cross-origin']
|
||||
details.responseHeaders['Cross-Origin-Embedder-Policy'] = ['require-corp']
|
||||
}
|
||||
callback({ responseHeaders: details.responseHeaders })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** A WebView created in a renderer process that does not have Node.js integration enabled will not
|
||||
* be able to enable integration itself. However, a WebView will always create an independent
|
||||
* renderer process with its own webPreferences. It is a good idea to control the creation of new
|
||||
@ -131,6 +162,7 @@ function disableNewWindowsCreation() {
|
||||
export function enableAll() {
|
||||
enableGlobalSandbox()
|
||||
rejectPermissionRequests()
|
||||
addMissingCorsHeaders()
|
||||
limitWebViewCreation()
|
||||
preventNavigation()
|
||||
disableNewWindowsCreation()
|
||||
|
@ -12,10 +12,12 @@
|
||||
<!-- FIXME https://github.com/validator/validator/issues/917 -->
|
||||
<!-- FIXME Security Vulnerabilities: https://github.com/enso-org/ide/issues/226 -->
|
||||
<!-- NOTE `frame-src` section of `http-equiv` required only for authorization -->
|
||||
<!-- NOTE [NP]: https://stripe.com/docs/security/guide#content-security-policy for Stripe.js -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="
|
||||
default-src 'self';
|
||||
frame-src 'self' data: https://js.stripe.com;
|
||||
script-src 'self' 'unsafe-eval' data: https://*;
|
||||
style-src 'self' 'unsafe-inline' data: https://*;
|
||||
connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://* wss://*;
|
||||
|
@ -12,11 +12,12 @@
|
||||
<!-- FIXME https://github.com/validator/validator/issues/917 -->
|
||||
<!-- FIXME Security Vulnerabilities: https://github.com/enso-org/ide/issues/226 -->
|
||||
<!-- NOTE `frame-src` section of `http-equiv` required only for authorization -->
|
||||
<!-- NOTE [NP]: https://stripe.com/docs/security/guide#content-security-policy for Stripe.js -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="
|
||||
default-src 'self';
|
||||
frame-src 'self' data: https://accounts.google.com https://enso-org.firebaseapp.com;
|
||||
frame-src 'self' data: https://js.stripe.com;
|
||||
script-src 'self' 'unsafe-eval' data: https://*;
|
||||
style-src 'self' 'unsafe-inline' data: https://*;
|
||||
connect-src 'self' data: ws://localhost:* ws://127.0.0.1:* http://localhost:* https://* wss://*;
|
||||
|
@ -61,6 +61,7 @@ import Registration from '#/pages/authentication/Registration'
|
||||
import ResetPassword from '#/pages/authentication/ResetPassword'
|
||||
import SetUsername from '#/pages/authentication/SetUsername'
|
||||
import Dashboard from '#/pages/dashboard/Dashboard'
|
||||
import Subscribe from '#/pages/subscribe/Subscribe'
|
||||
|
||||
import type Backend from '#/services/Backend'
|
||||
import LocalBackend from '#/services/LocalBackend'
|
||||
@ -226,6 +227,7 @@ function AppRouter(props: AppProps) {
|
||||
path={appUtils.DASHBOARD_PATH}
|
||||
element={shouldShowDashboard && <Dashboard {...props} />}
|
||||
/>
|
||||
<router.Route path={appUtils.SUBSCRIBE_PATH} element={<Subscribe />} />
|
||||
</router.Route>
|
||||
{/* Semi-protected pages are visible to users currently registering. */}
|
||||
<router.Route element={<authProvider.SemiProtectedLayout />}>
|
||||
|
@ -20,8 +20,11 @@ export const RESET_PASSWORD_PATH = '/password-reset'
|
||||
export const SET_USERNAME_PATH = '/set-username'
|
||||
/** Path to the offline mode entrypoint. */
|
||||
export const ENTER_OFFLINE_MODE_PATH = '/offline'
|
||||
/** Path to page in which the currently active payment plan can be managed. */
|
||||
export const SUBSCRIBE_PATH = '/subscribe'
|
||||
/** A {@link RegExp} matching all paths. */
|
||||
export const ALL_PATHS_REGEX = new RegExp(
|
||||
`(?:${DASHBOARD_PATH}|${LOGIN_PATH}|${REGISTRATION_PATH}|${CONFIRM_REGISTRATION_PATH}|` +
|
||||
`${FORGOT_PASSWORD_PATH}|${RESET_PASSWORD_PATH}|${SET_USERNAME_PATH})$`
|
||||
`${FORGOT_PASSWORD_PATH}|${RESET_PASSWORD_PATH}|${SET_USERNAME_PATH}|` +
|
||||
`${ENTER_OFFLINE_MODE_PATH}|${SUBSCRIBE_PATH})$`
|
||||
)
|
||||
|
@ -65,11 +65,18 @@ const BASE_AMPLIFY_CONFIG = {
|
||||
const AMPLIFY_CONFIGS = {
|
||||
/** Configuration for @indiv0's Cognito user pool. */
|
||||
npekin: {
|
||||
userPoolId: auth.UserPoolId('eu-west-1_7yB1Lr0fS'),
|
||||
userPoolWebClientId: auth.UserPoolWebClientId('ulc9knbbf0anduetrq9nnrlg2'),
|
||||
userPoolId: auth.UserPoolId('eu-west-1_xdBLmpah9'),
|
||||
userPoolWebClientId: auth.UserPoolWebClientId('1a7bner2d5ured09anh39sqhho'),
|
||||
domain: auth.OAuthDomain('npekin-enso-domain.auth.eu-west-1.amazoncognito.com'),
|
||||
...BASE_AMPLIFY_CONFIG,
|
||||
} satisfies Partial<auth.AmplifyConfig>,
|
||||
/** Configuration for @indiv0's Cognito user pool. */
|
||||
npekin2: {
|
||||
userPoolId: auth.UserPoolId('eu-west-1_FWbSTq3fV'),
|
||||
userPoolWebClientId: auth.UserPoolWebClientId('1e54ioss6ct6qfuek44g8hi1p0'),
|
||||
domain: auth.OAuthDomain('npekin2-enso-domain.auth.eu-west-1.amazoncognito.com'),
|
||||
...BASE_AMPLIFY_CONFIG,
|
||||
} satisfies Partial<auth.AmplifyConfig>,
|
||||
/** Configuration for @pbuchu's Cognito user pool. */
|
||||
pbuchu: {
|
||||
userPoolId: auth.UserPoolId('eu-west-1_jSF1RbgPK'),
|
||||
|
@ -20,6 +20,7 @@ import * as validation from '#/utilities/validation'
|
||||
|
||||
/** A modal for changing the user's password. */
|
||||
export default function ChangePasswordModal() {
|
||||
const { user } = authProvider.useNonPartialUserSession()
|
||||
const { changePassword } = authProvider.useAuth()
|
||||
const { unsetModal } = modalProvider.useSetModal()
|
||||
|
||||
@ -47,6 +48,7 @@ export default function ChangePasswordModal() {
|
||||
}}
|
||||
>
|
||||
<div className="self-center text-xl">Change your password</div>
|
||||
<input type="text" autoComplete="username" hidden value={user?.email} />
|
||||
<Input
|
||||
autoFocus
|
||||
required
|
||||
|
164
app/ide-desktop/lib/dashboard/src/pages/subscribe/Subscribe.tsx
Normal file
164
app/ide-desktop/lib/dashboard/src/pages/subscribe/Subscribe.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
/** @file A page in which the currently active payment plan can be changed. */
|
||||
import * as React from 'react'
|
||||
|
||||
import * as stripeReact from '@stripe/react-stripe-js'
|
||||
import type * as stripeTypes from '@stripe/stripe-js'
|
||||
import * as stripe from '@stripe/stripe-js/pure'
|
||||
import * as toast from 'react-toastify'
|
||||
|
||||
import * as appUtils from '#/appUtils'
|
||||
|
||||
import * as navigateHooks from '#/hooks/navigateHooks'
|
||||
import * as toastAndLogHooks from '#/hooks/toastAndLogHooks'
|
||||
|
||||
import * as backendProvider from '#/providers/BackendProvider'
|
||||
|
||||
import Modal from '#/components/Modal'
|
||||
|
||||
import * as backendModule from '#/services/Backend'
|
||||
|
||||
import * as config from '#/utilities/config'
|
||||
import * as load from '#/utilities/load'
|
||||
import * as string from '#/utilities/string'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
let stripePromise: Promise<stripeTypes.Stripe | null> | null = null
|
||||
|
||||
/** The delay in milliseconds before redirecting back to the main page. */
|
||||
const REDIRECT_DELAY_MS = 1_500
|
||||
|
||||
// =================
|
||||
// === Subscribe ===
|
||||
// =================
|
||||
|
||||
/** A page in which the currently active payment plan can be changed.
|
||||
*
|
||||
* This page can be in one of several states:
|
||||
*
|
||||
* 1. Initial (i.e. `plan = null, clientSecret = '', sessionStatus = null`),
|
||||
* 2. Plan selected (e.g. `plan = 'solo', clientSecret = '', sessionStatus = null`),
|
||||
* 3. Session created (e.g. `plan = 'solo', clientSecret = 'cs_foo',
|
||||
* sessionStatus.status = { status: 'open' || 'complete' || 'expired',
|
||||
* paymentStatus: 'no_payment_required' || 'paid' || 'unpaid' }`),
|
||||
* 4. Session complete (e.g. `plan = 'solo', clientSecret = 'cs_foo',
|
||||
* sessionStatus.status = { status: 'complete',
|
||||
* paymentStatus: 'no_payment_required' || 'paid' || 'unpaid' }`). */
|
||||
export default function Subscribe() {
|
||||
const stripeKey = config.ACTIVE_CONFIG.stripeKey
|
||||
const navigate = navigateHooks.useNavigate()
|
||||
// Plan that the user has currently selected, if any (e.g., 'solo', 'team', etc.).
|
||||
const [plan, setPlan] = React.useState(() => {
|
||||
const initialPlan = new URLSearchParams(location.search).get('plan')
|
||||
return backendModule.isPlan(initialPlan) ? initialPlan : backendModule.Plan.solo
|
||||
})
|
||||
// A client secret used to access details about a Checkout Session on the Stripe API. A Checkout
|
||||
// Session represents a customer's session as they are in the process of paying for a
|
||||
// subscription. The client secret is provided by Stripe when the Checkout Session is created.
|
||||
const [clientSecret, setClientSecret] = React.useState('')
|
||||
// The ID of a Checkout Session on the Stripe API. This is the same as the client secret, minus
|
||||
// the secret part. Without the secret part, the session ID can be safely stored in the URL
|
||||
// query.
|
||||
const [sessionId, setSessionId] = React.useState<backendModule.CheckoutSessionId | null>(null)
|
||||
// The status of a Checkout Session on the Stripe API. This stores whether or not the Checkout
|
||||
// Session is complete (i.e., the user has provided payment information), and if so, whether
|
||||
// payment has been confirmed.
|
||||
const [sessionStatus, setSessionStatus] =
|
||||
React.useState<backendModule.CheckoutSessionStatus | null>(null)
|
||||
const { backend } = backendProvider.useBackend()
|
||||
const toastAndLog = toastAndLogHooks.useToastAndLog()
|
||||
|
||||
if (stripePromise == null) {
|
||||
stripePromise = load.loadScript('https://js.stripe.com/v3/').then(async script => {
|
||||
const innerStripe = await stripe.loadStripe(stripeKey)
|
||||
script.remove()
|
||||
return innerStripe
|
||||
})
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const checkoutSession = await backend.createCheckoutSession(plan)
|
||||
setClientSecret(checkoutSession.clientSecret)
|
||||
setSessionId(checkoutSession.id)
|
||||
} catch (error) {
|
||||
toastAndLog(null, error)
|
||||
}
|
||||
})()
|
||||
}, [backend, plan, /* should never change */ toastAndLog])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (sessionStatus?.status === 'complete') {
|
||||
toast.toast.success('Your plan has successfully been upgraded!')
|
||||
window.setTimeout(() => {
|
||||
navigate(appUtils.DASHBOARD_PATH)
|
||||
}, REDIRECT_DELAY_MS)
|
||||
}
|
||||
}, [sessionStatus?.status, navigate])
|
||||
|
||||
const onComplete = React.useCallback(() => {
|
||||
if (sessionId != null) {
|
||||
void (async () => {
|
||||
try {
|
||||
setSessionStatus(await backend.getCheckoutSession(sessionId))
|
||||
} catch (error) {
|
||||
toastAndLog(null, error)
|
||||
}
|
||||
})()
|
||||
}
|
||||
// Stripe does not allow this callback to change.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessionId])
|
||||
|
||||
return (
|
||||
<Modal centered className="bg-black/10 text-primary text-xs">
|
||||
<div
|
||||
data-testid="subscribe-modal"
|
||||
className="flex flex-col gap-2 bg-frame-selected backdrop-blur-3xl rounded-2xl p-8 w-full max-w-md max-h-[100vh]"
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div className="self-center text-xl">Upgrade to {string.capitalizeFirst(plan)}</div>
|
||||
<div className="flex items-stretch rounded-full bg-gray-500/30 text-base h-8">
|
||||
{backendModule.PLANS.map(newPlan => (
|
||||
<button
|
||||
key={newPlan}
|
||||
disabled={plan === newPlan}
|
||||
type="button"
|
||||
className="flex-1 grow rounded-full disabled:bg-frame"
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
setPlan(newPlan)
|
||||
}}
|
||||
>
|
||||
{string.capitalizeFirst(newPlan)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{sessionId && clientSecret ? (
|
||||
<div className="overflow-auto">
|
||||
<stripeReact.EmbeddedCheckoutProvider
|
||||
key={sessionId}
|
||||
stripe={stripePromise}
|
||||
// Above, `sessionId` is updated when the `checkoutSession` is created.
|
||||
// This triggers a fetch of the session's `status`.
|
||||
// The `status` is not going to be `complete` at that point
|
||||
// (unless the user completes the checkout process before the fetch is complete).
|
||||
// So the `status` needs to be fetched again when the `checkoutSession` is updated.
|
||||
// This is done by passing a function to `onComplete`.
|
||||
options={{ clientSecret, onComplete }}
|
||||
>
|
||||
<stripeReact.EmbeddedCheckout />
|
||||
</stripeReact.EmbeddedCheckoutProvider>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-155 transition-all"></div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
/** @file Type definitions common between all backends. */
|
||||
import type * as React from 'react'
|
||||
|
||||
import * as array from '#/utilities/array'
|
||||
import * as dateTime from '#/utilities/dateTime'
|
||||
import * as newtype from '#/utilities/newtype'
|
||||
import * as permissions from '#/utilities/permissions'
|
||||
@ -49,6 +50,10 @@ export const ConnectorId = newtype.newtypeConstructor<ConnectorId>()
|
||||
/** Unique identifier for an arbitrary asset. */
|
||||
export type AssetId = IdType[keyof IdType]
|
||||
|
||||
/** Unique identifier for a payment checkout session. */
|
||||
export type CheckoutSessionId = newtype.Newtype<string, 'CheckoutSessionId'>
|
||||
export const CheckoutSessionId = newtype.newtypeConstructor<CheckoutSessionId>()
|
||||
|
||||
/** The name of an asset label. */
|
||||
export type LabelName = newtype.Newtype<string, 'LabelName'>
|
||||
export const LabelName = newtype.newtypeConstructor<LabelName>()
|
||||
@ -310,6 +315,34 @@ export interface Version {
|
||||
readonly version_type: VersionType
|
||||
}
|
||||
|
||||
/** Subscription plans. */
|
||||
export enum Plan {
|
||||
solo = 'solo',
|
||||
team = 'team',
|
||||
}
|
||||
|
||||
export const PLANS = Object.values(Plan)
|
||||
|
||||
// This is a function, even though it does not look like one.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
export const isPlan = array.includesPredicate(PLANS)
|
||||
|
||||
/** Metadata uniquely describing a payment checkout session. */
|
||||
export interface CheckoutSession {
|
||||
/** ID of the checkout session, suffixed with a secret value. */
|
||||
readonly clientSecret: string
|
||||
/** ID of the checkout session. */
|
||||
readonly id: CheckoutSessionId
|
||||
}
|
||||
|
||||
/** Metadata describing the status of a payment checkout session. */
|
||||
export interface CheckoutSessionStatus {
|
||||
/** Status of the payment for the checkout session. */
|
||||
readonly paymentStatus: string
|
||||
/** Status of the checkout session. */
|
||||
readonly status: string
|
||||
}
|
||||
|
||||
/** Resource usage of a VM. */
|
||||
export interface ResourceUsage {
|
||||
/** Percentage of memory used. */
|
||||
@ -859,6 +892,11 @@ export interface CreateTagRequestBody {
|
||||
readonly color: LChColor
|
||||
}
|
||||
|
||||
/** HTTP request body for the "create checkout session" endpoint. */
|
||||
export interface CreateCheckoutSessionRequestBody {
|
||||
plan: Plan
|
||||
}
|
||||
|
||||
/** URL query string parameters for the "list directory" endpoint. */
|
||||
export interface ListDirectoryRequestParams {
|
||||
readonly parentId: string | null
|
||||
@ -1086,4 +1124,8 @@ export default abstract class Backend {
|
||||
abstract deleteTag(tagId: TagId, value: LabelName): Promise<void>
|
||||
/** Return a list of backend or IDE versions. */
|
||||
abstract listVersions(params: ListVersionsRequestParams): Promise<Version[]>
|
||||
/** Create a payment checkout session. */
|
||||
abstract createCheckoutSession(plan: Plan): Promise<CheckoutSession>
|
||||
/** Get the status of a payment checkout session. */
|
||||
abstract getCheckoutSession(sessionId: CheckoutSessionId): Promise<CheckoutSessionStatus>
|
||||
}
|
||||
|
@ -475,4 +475,14 @@ export default class LocalBackend extends Backend {
|
||||
override deleteTag() {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
/** Invalid operation. */
|
||||
override createCheckoutSession() {
|
||||
return this.invalidOperation()
|
||||
}
|
||||
|
||||
/** Invalid operation. */
|
||||
override getCheckoutSession() {
|
||||
return this.invalidOperation()
|
||||
}
|
||||
}
|
||||
|
@ -856,6 +856,36 @@ export default class RemoteBackend extends Backend {
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a payment checkout session.
|
||||
* @throws An error if a non-successful status code (not 200-299) was received. */
|
||||
override async createCheckoutSession(
|
||||
plan: backendModule.Plan
|
||||
): Promise<backendModule.CheckoutSession> {
|
||||
const response = await this.post<backendModule.CheckoutSession>(
|
||||
remoteBackendPaths.CREATE_CHECKOUT_SESSION_PATH,
|
||||
{ plan } satisfies backendModule.CreateCheckoutSessionRequestBody
|
||||
)
|
||||
if (!responseIsSuccessful(response)) {
|
||||
return this.throw(`Could not create checkout session for plan '${plan}'.`)
|
||||
} else {
|
||||
return await response.json()
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the status of a payment checkout session.
|
||||
* @throws An error if a non-successful status code (not 200-299) was received. */
|
||||
override async getCheckoutSession(
|
||||
sessionId: backendModule.CheckoutSessionId
|
||||
): Promise<backendModule.CheckoutSessionStatus> {
|
||||
const path = remoteBackendPaths.getCheckoutSessionPath(sessionId)
|
||||
const response = await this.get<backendModule.CheckoutSessionStatus>(path)
|
||||
if (!responseIsSuccessful(response)) {
|
||||
return this.throw(`Could not get checkout session for session ID '${sessionId}'.`)
|
||||
} else {
|
||||
return await response.json()
|
||||
}
|
||||
}
|
||||
|
||||
/** Get the default version given the type of version (IDE or backend). */
|
||||
protected async getDefaultVersion(versionType: backendModule.VersionType) {
|
||||
const cached = this.defaultVersions[versionType]
|
||||
|
@ -55,6 +55,10 @@ export const CREATE_TAG_PATH = 'tags'
|
||||
export const LIST_TAGS_PATH = 'tags'
|
||||
/** Relative HTTP path to the "list versions" endpoint of the Cloud backend API. */
|
||||
export const LIST_VERSIONS_PATH = 'versions'
|
||||
/** Relative HTTP path to the "create checkout session" endpoint of the Cloud backend API. */
|
||||
export const CREATE_CHECKOUT_SESSION_PATH = 'payments/checkout-sessions'
|
||||
/** Relative HTTP path to the "get checkout session" endpoint of the Cloud backend API. */
|
||||
export const GET_CHECKOUT_SESSION_PATH = 'payments/checkout-sessions'
|
||||
/** Relative HTTP path to the "list asset versions" endpoint of the Cloud backend API. */
|
||||
export function listAssetVersionsPath(assetId: backend.AssetId) {
|
||||
return `assets/${assetId}/versions`
|
||||
@ -119,3 +123,7 @@ export function associateTagPath(assetId: backend.AssetId) {
|
||||
export function deleteTagPath(tagId: backend.TagId) {
|
||||
return `tags/${tagId}`
|
||||
}
|
||||
/** Relative HTTP path to the "get checkout session" endpoint of the Cloud backend API. */
|
||||
export function getCheckoutSessionPath(checkoutSessionId: backend.CheckoutSessionId) {
|
||||
return `payments/checkout-sessions/${checkoutSessionId}`
|
||||
}
|
||||
|
@ -46,7 +46,8 @@ const CLOUD_REDIRECTS = {
|
||||
/** All possible API URLs, sorted by environment. */
|
||||
const API_URLS = {
|
||||
pbuchu: ApiUrl('https://xw0g8j3tsb.execute-api.eu-west-1.amazonaws.com'),
|
||||
npekin: ApiUrl('https://lkxuay3ha1.execute-api.eu-west-1.amazonaws.com'),
|
||||
npekin: ApiUrl('https://opk1cxpwec.execute-api.eu-west-1.amazonaws.com'),
|
||||
npekin2: ApiUrl('https://8rf1a7iy49.execute-api.eu-west-1.amazonaws.com'),
|
||||
production: ApiUrl('https://7aqkn3tnbc.execute-api.eu-west-1.amazonaws.com'),
|
||||
}
|
||||
|
||||
@ -60,22 +61,41 @@ const CHAT_URLS = {
|
||||
production: ChatUrl('wss://chat.cloud.enso.org'),
|
||||
}
|
||||
|
||||
/** All possible Stripe API public keys, sorted by environment. */
|
||||
const STRIPE_KEYS = {
|
||||
npekin:
|
||||
'pk_test_51O8REgAjUAkYBrsQooU5iMWumr7D4Vf9H2A671A8zXV87VwDOTenDbJx5g3PN9IjgkbK6omxlp01bGfghA3qZSIu00lYsytprU',
|
||||
development:
|
||||
'pk_test_51Iv1a0FpIovSdxvQBRpzZpfikr7CD6DFWFF8g2ycjut3d9PXD2Jc9I2j3G1DWWgMfaNzzHyXtvUr2GaNkuQayEzu00YHYfKtGC',
|
||||
production:
|
||||
'pk_test_51Iv3YNB30SZwisesLWKD1KCUmkkOy2Bbq0zwYO56zgSjdIf00Bw39BC1Zvn4PPjq6GHZd8Q8oaR6M0JlC1K9b1f1007cjnwi4e',
|
||||
} as const
|
||||
|
||||
/** All possible configuration options, sorted by environment. */
|
||||
const CONFIGS = {
|
||||
npekin: {
|
||||
cloudRedirect: CLOUD_REDIRECTS.development,
|
||||
apiUrl: API_URLS.npekin,
|
||||
chatUrl: CHAT_URLS.development,
|
||||
stripeKey: STRIPE_KEYS.npekin,
|
||||
} satisfies Config,
|
||||
npekin2: {
|
||||
cloudRedirect: CLOUD_REDIRECTS.development,
|
||||
apiUrl: API_URLS.npekin2,
|
||||
chatUrl: CHAT_URLS.development,
|
||||
stripeKey: STRIPE_KEYS.npekin,
|
||||
} satisfies Config,
|
||||
pbuchu: {
|
||||
cloudRedirect: CLOUD_REDIRECTS.development,
|
||||
apiUrl: API_URLS.pbuchu,
|
||||
chatUrl: CHAT_URLS.development,
|
||||
stripeKey: STRIPE_KEYS.development,
|
||||
} satisfies Config,
|
||||
production: {
|
||||
cloudRedirect: CLOUD_REDIRECTS.production,
|
||||
apiUrl: API_URLS.production,
|
||||
chatUrl: CHAT_URLS.production,
|
||||
stripeKey: STRIPE_KEYS.production,
|
||||
} satisfies Config,
|
||||
}
|
||||
/** Export the configuration that is currently in use. */
|
||||
@ -95,6 +115,10 @@ export interface Config {
|
||||
readonly apiUrl: ApiUrl
|
||||
/** URL to the websocket endpoint of the Help Chat. */
|
||||
readonly chatUrl: ChatUrl
|
||||
/** Key used to authenticate with the Stripe API. Must point at the same Stripe account that the
|
||||
* backend is configured to use (though the backend will use a secret key, while this component
|
||||
* uses a public key). */
|
||||
readonly stripeKey: string
|
||||
}
|
||||
|
||||
// ===================
|
||||
@ -103,4 +127,4 @@ export interface Config {
|
||||
|
||||
/** Possible values for the environment/user we're running for and whose infrastructure we're
|
||||
* testing against. */
|
||||
export type Environment = 'npekin' | 'pbuchu' | 'production'
|
||||
export type Environment = 'npekin' | 'npekin2' | 'pbuchu' | 'production'
|
||||
|
@ -103,6 +103,7 @@ export default /** @satisfies {import('tailwindcss').Config} */ ({
|
||||
115.25: '28.8125rem',
|
||||
120: '30rem',
|
||||
140: '35rem',
|
||||
155: '38.75rem',
|
||||
'10lh': '10lh',
|
||||
},
|
||||
minHeight: {
|
||||
|
@ -5,8 +5,6 @@ import vitePluginYaml from '@modyfi/vite-plugin-yaml'
|
||||
import vitePluginReact from '@vitejs/plugin-react'
|
||||
import * as vite from 'vite'
|
||||
|
||||
import * as common from 'enso-common'
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
@ -20,7 +18,7 @@ const SERVER_PORT = 8080
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
export default vite.defineConfig({
|
||||
server: { port: SERVER_PORT, headers: Object.fromEntries(common.COOP_COEP_CORP_HEADERS) },
|
||||
server: { port: SERVER_PORT },
|
||||
plugins: [
|
||||
vitePluginReact({
|
||||
include: '**/*.tsx',
|
||||
|
@ -32,6 +32,8 @@
|
||||
"lint": "npm run --workspace=enso-gui2 compile-server && npm run lint-only"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stripe/react-stripe-js": "^2.3.1",
|
||||
"@stripe/stripe-js": "^2.1.10",
|
||||
"esbuild-plugin-inline-image": "^0.0.9",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
|
20
package-lock.json
generated
20
package-lock.json
generated
@ -173,6 +173,8 @@
|
||||
"name": "enso-ide-desktop",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@stripe/react-stripe-js": "^2.3.1",
|
||||
"@stripe/stripe-js": "^2.1.10",
|
||||
"esbuild-plugin-inline-image": "^0.0.9",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
@ -3918,6 +3920,24 @@
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/react-stripe-js": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-2.5.0.tgz",
|
||||
"integrity": "sha512-ys5bnrufNoBlUPRblWkiN7dUCX3s2noBZS0Lf5GkHYO9x3SXJMZccif53IdP4X6tZyNLn7aSMgZBUZqsdzrvhg==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@stripe/stripe-js": "^1.44.1 || ^2.0.0 || ^3.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stripe/stripe-js": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-2.4.0.tgz",
|
||||
"integrity": "sha512-WFkQx1mbs2b5+7looI9IV1BLa3bIApuN3ehp9FP58xGg7KL9hCHDECgW3BwO9l9L+xBPVAD7Yjn1EhGe6EDTeA=="
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "4.0.6",
|
||||
"dev": true,
|
||||
|
Loading…
Reference in New Issue
Block a user