Import code for payments.ente.io
Manav: To avoid accidentally spilling out any secrets, squash and import. However, the overwhelming majority of the work in the original code was done by Abhinav <abhinavk.grd@gmail.com> so set them as the author for the commit.
This commit is contained in:
parent
33314bc2da
commit
8a3c0743fe
37 changed files with 4557 additions and 0 deletions
13
web/apps/payments/.babelrc
Normal file
13
web/apps/payments/.babelrc
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": [
|
||||
[
|
||||
"styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": true,
|
||||
"preprocess": false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
3
web/apps/payments/.eslintrc
Normal file
3
web/apps/payments/.eslintrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["next", "next/core-web-vitals"]
|
||||
}
|
3
web/apps/payments/.github/pull_request_template.md
vendored
Normal file
3
web/apps/payments/.github/pull_request_template.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
## Description
|
||||
|
||||
## Test Plan
|
34
web/apps/payments/.gitignore
vendored
Normal file
34
web/apps/payments/.gitignore
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
6
web/apps/payments/.prettierrc.json
Normal file
6
web/apps/payments/.prettierrc.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"jsxBracketSameLine": true
|
||||
}
|
59
web/apps/payments/README.md
Normal file
59
web/apps/payments/README.md
Normal file
|
@ -0,0 +1,59 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with
|
||||
[`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
If you're running this to test out the payment flows end-to-end, please do a
|
||||
`yarn build`, that will place the output within the `out` folder.
|
||||
|
||||
Then use any tool to serve this over HTTP. For example, `python3 -m http.server
|
||||
3001` will serve this directory over port `3001`.
|
||||
|
||||
Aside that, these are the necessary configuration changes.
|
||||
|
||||
### Local configuration
|
||||
|
||||
Update the `.env.local` to point to the local museum instance, and to define the
|
||||
necessary Stripe keys that can be fetched from [Stripe's developer
|
||||
dashboard](https://dashboard.stripe.com).
|
||||
|
||||
Assuming that your local museum instance is running on `192.168.1.2:8080`, your
|
||||
`.env.local` should look as follows.
|
||||
|
||||
```
|
||||
NEXT_PUBLIC_ENTE_ENDPOINT = http://192.168.1.2:8080
|
||||
NEXT_PUBLIC_STRIPE_US_PUBLISHABLE_KEY = stripe_publishable_key
|
||||
```
|
||||
|
||||
### Museum
|
||||
|
||||
1. Install the [stripe-cli](https://docs.stripe.com/stripe-cli) and capture the
|
||||
webhook signing secret.
|
||||
|
||||
2. Define this secret within your `musuem.yaml`
|
||||
|
||||
3. Update the `whitelisted-redirect-urls` so that it supports redirecting to this locally running project
|
||||
|
||||
Assuming that your local payments app is running on `192.168.1.2:3001`, your
|
||||
`museum.yaml` should look as follows.
|
||||
|
||||
```yaml
|
||||
stripe:
|
||||
us:
|
||||
key: stripe_dev_key
|
||||
webhook-secret: stripe_dev_webhook_secret
|
||||
whitelisted-redirect-urls: ["http://192.168.1.2:3001/frameRedirect"]
|
||||
path:
|
||||
success: ?status=success&session_id={CHECKOUT_SESSION_ID}
|
||||
cancel: ?status=fail&reason=canceled
|
||||
```
|
44
web/apps/payments/SECURITY.md
Normal file
44
web/apps/payments/SECURITY.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
ente believes that working with security researchers across the globe is crucial to keeping our
|
||||
users safe. If you believe you've found a security issue in our product or service, we encourage you to
|
||||
notify us (security@ente.io). We welcome working with you to resolve the issue promptly. Thanks in advance!
|
||||
|
||||
# Disclosure Policy
|
||||
|
||||
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
|
||||
effort to quickly resolve the issue.
|
||||
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
|
||||
third-party. We may publicly disclose the issue before resolving it, if appropriate.
|
||||
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
|
||||
degradation of our service. Only interact with accounts you own or with explicit permission of the
|
||||
account holder.
|
||||
- If you would like to encrypt your report, please use the PGP key with long ID
|
||||
`E273695C0403F34F74171932DF6DDDE98EBD2394` (available in the public keyserver pool).
|
||||
|
||||
# In-scope
|
||||
|
||||
- Security issues in any current release of ente. This includes the web app, desktop app,
|
||||
and mobile apps (iOS and Android). Product downloads are available at https://ente.io. Source
|
||||
code is available at https://github.com/ente-io.
|
||||
|
||||
# Exclusions
|
||||
|
||||
The following bug classes are out-of scope:
|
||||
|
||||
- Bugs that are already reported on any of ente's issue trackers (https://github.com/ente-io),
|
||||
or that we already know of. Note that some of our issue tracking is private.
|
||||
- Issues in an upstream software dependency (ex: Flutter, Next.js etc) which are already reported to the upstream maintainer.
|
||||
- Attacks requiring physical access to a user's device.
|
||||
- Self-XSS
|
||||
- Issues related to software or protocols not under ente's control
|
||||
- Vulnerabilities in outdated versions of ente
|
||||
- Missing security best practices that do not directly lead to a vulnerability
|
||||
- Issues that do not have any impact on the general public
|
||||
|
||||
While researching, we'd like to ask you to refrain from:
|
||||
|
||||
- Denial of service
|
||||
- Spamming
|
||||
- Social engineering (including phishing) of ente staff or contractors
|
||||
- Any physical attempts against ente property or data centers
|
||||
|
||||
Thank you for helping keep ente and our users safe!
|
44
web/apps/payments/next.config.js
Normal file
44
web/apps/payments/next.config.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
// This file sets a custom webpack configuration to use your Next.js app
|
||||
// with Sentry.
|
||||
// https://nextjs.org/docs/api-reference/next.config.js/introduction
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
const { withSentryConfig } = require('@sentry/nextjs');
|
||||
|
||||
const cp = require('child_process');
|
||||
const gitSha = cp.execSync('git rev-parse --short HEAD', {
|
||||
cwd: __dirname,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
|
||||
const moduleExports = {
|
||||
// Your existing module.exports
|
||||
output: 'export',
|
||||
reactStrictMode: true,
|
||||
env: {
|
||||
SENTRY_RELEASE: gitSha,
|
||||
},
|
||||
sentry: {
|
||||
hideSourceMaps: false,
|
||||
},
|
||||
};
|
||||
|
||||
const SentryWebpackPluginOptions = {
|
||||
// Additional config options for the Sentry Webpack plugin. Keep in mind that
|
||||
// the following options are set automatically, and overriding them is not
|
||||
// recommended:
|
||||
// release, url, org, project, authToken, configFile, stripPrefix,
|
||||
// urlPrefix, include, ignore
|
||||
release: gitSha,
|
||||
silent: true, // Suppresses all logs
|
||||
// Ignore sentry webpack errors
|
||||
errorHandler: (err, invokeErr, compilation) => {
|
||||
compilation.warnings.push('Sentry CLI Plugin: ' + err.message);
|
||||
},
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
||||
};
|
||||
|
||||
// Make sure adding Sentry options is the last code to run before exporting, to
|
||||
// ensure that your source maps include changes from all other Webpack plugins
|
||||
module.exports = withSentryConfig(moduleExports, SentryWebpackPluginOptions);
|
31
web/apps/payments/package.json
Normal file
31
web/apps/payments/package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "payments",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3001",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/nextjs": "^7.54.0",
|
||||
"@stripe/stripe-js": "^1.17.0",
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "4.6.0",
|
||||
"next": "^14.1.4",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^1.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"styled-components": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.11.30",
|
||||
"@types/react": "17.0.15",
|
||||
"@types/styled-components": "^5.1.12",
|
||||
"babel-plugin-styled-components": "^1.13.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.1.4",
|
||||
"typescript": "^5.4.2"
|
||||
}
|
||||
}
|
96
web/apps/payments/public/fonts/UFL.txt
Normal file
96
web/apps/payments/public/fonts/UFL.txt
Normal file
|
@ -0,0 +1,96 @@
|
|||
-------------------------------
|
||||
UBUNTU FONT LICENCE Version 1.0
|
||||
-------------------------------
|
||||
|
||||
PREAMBLE
|
||||
This licence allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely. The fonts, including any derivative works, can be
|
||||
bundled, embedded, and redistributed provided the terms of this licence
|
||||
are met. The fonts and derivatives, however, cannot be released under
|
||||
any other licence. The requirement for fonts to remain under this
|
||||
licence does not require any document created using the fonts or their
|
||||
derivatives to be published under this licence, as long as the primary
|
||||
purpose of the document is not to be a vehicle for the distribution of
|
||||
the fonts.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this licence and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Original Version" refers to the collection of Font Software components
|
||||
as received under this licence.
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to
|
||||
a new environment.
|
||||
|
||||
"Copyright Holder(s)" refers to all individuals and companies who have a
|
||||
copyright ownership of the Font Software.
|
||||
|
||||
"Substantially Changed" refers to Modified Versions which can be easily
|
||||
identified as dissimilar to the Font Software by users of the Font
|
||||
Software comparing the Original Version with the Modified Version.
|
||||
|
||||
To "Propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification and with or without charging
|
||||
a redistribution fee), making available to the public, and in some
|
||||
countries other activities as well.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
This licence does not grant any rights under trademark law and all such
|
||||
rights are reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of the Font Software, to propagate the Font Software, subject to
|
||||
the below conditions:
|
||||
|
||||
1) Each copy of the Font Software must contain the above copyright
|
||||
notice and this licence. These can be included either as stand-alone
|
||||
text files, human-readable headers or in the appropriate machine-
|
||||
readable metadata fields within text or binary files as long as those
|
||||
fields can be easily viewed by the user.
|
||||
|
||||
2) The font name complies with the following:
|
||||
(a) The Original Version must retain its name, unmodified.
|
||||
(b) Modified Versions which are Substantially Changed must be renamed to
|
||||
avoid use of the name of the Original Version or similar names entirely.
|
||||
(c) Modified Versions which are not Substantially Changed must be
|
||||
renamed to both (i) retain the name of the Original Version and (ii) add
|
||||
additional naming elements to distinguish the Modified Version from the
|
||||
Original Version. The name of such Modified Versions must be the name of
|
||||
the Original Version, with "derivative X" where X represents the name of
|
||||
the new work, appended to that name.
|
||||
|
||||
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
||||
Font Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except (i) as required by this licence, (ii) to
|
||||
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
||||
their explicit written permission.
|
||||
|
||||
4) The Font Software, modified or unmodified, in part or in whole, must
|
||||
be distributed entirely under this licence, and must not be distributed
|
||||
under any other licence. The requirement for fonts to remain under this
|
||||
licence does not affect any document created using the Font Software,
|
||||
except any version of the Font Software extracted from a document
|
||||
created using the Font Software may only be distributed under this
|
||||
licence.
|
||||
|
||||
TERMINATION
|
||||
This licence becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||
DEALINGS IN THE FONT SOFTWARE.
|
BIN
web/apps/payments/public/fonts/ubuntu-v15-latin-700.woff
Normal file
BIN
web/apps/payments/public/fonts/ubuntu-v15-latin-700.woff
Normal file
Binary file not shown.
BIN
web/apps/payments/public/fonts/ubuntu-v15-latin-700.woff2
Normal file
BIN
web/apps/payments/public/fonts/ubuntu-v15-latin-700.woff2
Normal file
Binary file not shown.
BIN
web/apps/payments/public/fonts/ubuntu-v15-latin-regular.woff
Normal file
BIN
web/apps/payments/public/fonts/ubuntu-v15-latin-regular.woff
Normal file
Binary file not shown.
BIN
web/apps/payments/public/fonts/ubuntu-v15-latin-regular.woff2
Normal file
BIN
web/apps/payments/public/fonts/ubuntu-v15-latin-regular.woff2
Normal file
Binary file not shown.
27
web/apps/payments/sentry.client.config.js
Normal file
27
web/apps/payments/sentry.client.config.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
// This file configures the initialization of Sentry on the browser.
|
||||
// The config you add here will be used whenever a page is visited.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const SENTRY_DSN =
|
||||
(process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN) ??
|
||||
'https://67447bc36684b1f7a18d79683b788f25@sentry.ente.io/6';
|
||||
|
||||
const TUNNEL_URL = 'https://sentry-reporter.ente.io';
|
||||
const SENTRY_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development';
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
enabled: false,
|
||||
environment: SENTRY_ENV,
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
attachStacktrace: true,
|
||||
autoSessionTracking: false,
|
||||
tunnel: TUNNEL_URL,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
4
web/apps/payments/sentry.properties
Normal file
4
web/apps/payments/sentry.properties
Normal file
|
@ -0,0 +1,4 @@
|
|||
defaults.url=https://sentry.ente.io/
|
||||
defaults.org=ente
|
||||
defaults.project=web-payments
|
||||
|
26
web/apps/payments/sentry.server.config.js
Normal file
26
web/apps/payments/sentry.server.config.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever the server handles a request.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
|
||||
const SENTRY_DSN =
|
||||
(process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN) ??
|
||||
'https://208e398c28cd4c069c83d7c6e63adef6@sentry.ente.io/6';
|
||||
|
||||
const SENTRY_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development';
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
enabled: SENTRY_ENV !== 'development',
|
||||
environment: SENTRY_ENV,
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
release: process.env.SENTRY_RELEASE,
|
||||
autoSessionTracking: false,
|
||||
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
9
web/apps/payments/src/components/Container.tsx
Normal file
9
web/apps/payments/src/components/Container.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
10
web/apps/payments/src/components/EnteSpinner.tsx
Normal file
10
web/apps/payments/src/components/EnteSpinner.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import { Spinner } from 'react-bootstrap';
|
||||
|
||||
export default function EnteSpinner(props: any) {
|
||||
return (
|
||||
<Spinner {...props} animation="border" variant="success" role="status">
|
||||
<span className="sr-only">Loading...</span>
|
||||
</Spinner>
|
||||
);
|
||||
}
|
2
web/apps/payments/src/constants/common.ts
Normal file
2
web/apps/payments/src/constants/common.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const DESKTOP_REDIRECT_URL = 'ente://app/gallery';
|
||||
export const ENTE_WEBSITE_URL = 'https://ente.io';
|
7
web/apps/payments/src/pages/404.tsx
Normal file
7
web/apps/payments/src/pages/404.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Container } from 'components/Container';
|
||||
import React from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
export default function Home() {
|
||||
return <Container>{constants.NOT_FOUND}</Container>;
|
||||
}
|
18
web/apps/payments/src/pages/_app.tsx
Normal file
18
web/apps/payments/src/pages/_app.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import '../styles/globals.css';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import type { AppProps } from 'next/app';
|
||||
import React from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import Head from 'next/head';
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{constants.TITLE}</title>
|
||||
</Head>
|
||||
<Component {...pageProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default MyApp;
|
20
web/apps/payments/src/pages/desktop-redirect.tsx
Normal file
20
web/apps/payments/src/pages/desktop-redirect.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Container } from 'components/Container';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { DESKTOP_REDIRECT_URL } from 'constants/common';
|
||||
import { useRouter } from 'next/dist/client/router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export default function DesktopRedirect() {
|
||||
useEffect(() => {
|
||||
const currentURL = new URL(window.location.href);
|
||||
const desktopRedirectURL = new URL(DESKTOP_REDIRECT_URL);
|
||||
desktopRedirectURL.search = currentURL.search;
|
||||
window.location.href = desktopRedirectURL.href;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<EnteSpinner animation="border" />
|
||||
</Container>
|
||||
);
|
||||
}
|
39
web/apps/payments/src/pages/index.tsx
Normal file
39
web/apps/payments/src/pages/index.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Container } from 'components/Container';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { ENTE_WEBSITE_URL } from 'constants/common';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { parseAndHandleRequest } from 'services/billingService';
|
||||
import { CUSTOM_ERROR } from 'utils/error';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
export default function Home() {
|
||||
const [errorMessageView, setErrorMessageView] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
async function main() {
|
||||
try {
|
||||
setLoading(true);
|
||||
await parseAndHandleRequest();
|
||||
} catch (e: any) {
|
||||
if (
|
||||
e.message === CUSTOM_ERROR.DIRECT_OPEN_WITH_NO_QUERY_PARAMS
|
||||
) {
|
||||
window.location.href = ENTE_WEBSITE_URL;
|
||||
} else {
|
||||
setErrorMessageView(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
main();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{errorMessageView ? (
|
||||
<div>{constants.SOMETHING_WENT_WRONG}</div>
|
||||
) : (
|
||||
loading && <EnteSpinner animation="border" />
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
175
web/apps/payments/src/services/HTTPService.ts
Normal file
175
web/apps/payments/src/services/HTTPService.ts
Normal file
|
@ -0,0 +1,175 @@
|
|||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
|
||||
interface IHTTPHeaders {
|
||||
[headerKey: string]: any;
|
||||
}
|
||||
|
||||
interface IQueryPrams {
|
||||
[paramName: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to manage all HTTP calls.
|
||||
*/
|
||||
class HTTPService {
|
||||
constructor() {
|
||||
axios.interceptors.response.use(
|
||||
(response) => Promise.resolve(response),
|
||||
(err) => {
|
||||
if (!err.response) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
const { response } = err;
|
||||
return Promise.reject(response);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* header object to be append to all api calls.
|
||||
*/
|
||||
private headers: IHTTPHeaders = {
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the headers to the given object.
|
||||
*/
|
||||
public setHeaders(headers: IHTTPHeaders) {
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header to list of headers.
|
||||
*/
|
||||
public appendHeader(key: string, value: string) {
|
||||
this.headers = {
|
||||
...this.headers,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given header.
|
||||
*/
|
||||
public removeHeader(key: string) {
|
||||
this.headers[key] = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns axios interceptors.
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
public getInterceptors() {
|
||||
return axios.interceptors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic HTTP request.
|
||||
* This is done so that developer can use any functionality
|
||||
* provided by axios. Here, only the set headers are spread
|
||||
* over what was sent in config.
|
||||
*/
|
||||
public async request(config: AxiosRequestConfig, customConfig?: any) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers = {
|
||||
...this.headers,
|
||||
...config.headers,
|
||||
};
|
||||
if (customConfig?.cancel) {
|
||||
config.cancelToken=new axios.CancelToken((c)=> (customConfig.cancel.exec=c));
|
||||
}
|
||||
return await axios({ ...config, ...customConfig });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request.
|
||||
*/
|
||||
public get(
|
||||
url: string,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any,
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
headers,
|
||||
method: 'GET',
|
||||
params,
|
||||
url,
|
||||
},
|
||||
customConfig,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post request
|
||||
*/
|
||||
public post(
|
||||
url: string,
|
||||
data?: any,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any,
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
data,
|
||||
headers,
|
||||
method: 'POST',
|
||||
params,
|
||||
url,
|
||||
},
|
||||
customConfig,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Put request
|
||||
*/
|
||||
public put(
|
||||
url: string,
|
||||
data: any,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any,
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
data,
|
||||
headers,
|
||||
method: 'PUT',
|
||||
params,
|
||||
url,
|
||||
},
|
||||
customConfig,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete request
|
||||
*/
|
||||
public delete(
|
||||
url: string,
|
||||
data: any,
|
||||
params?: IQueryPrams,
|
||||
headers?: IHTTPHeaders,
|
||||
customConfig?: any,
|
||||
) {
|
||||
return this.request(
|
||||
{
|
||||
data,
|
||||
headers,
|
||||
method: 'DELETE',
|
||||
params,
|
||||
url,
|
||||
},
|
||||
customConfig,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a Singleton Service.
|
||||
// This will help me maintain common headers / functionality
|
||||
// at a central place.
|
||||
export default new HTTPService();
|
281
web/apps/payments/src/services/billingService.ts
Normal file
281
web/apps/payments/src/services/billingService.ts
Normal file
|
@ -0,0 +1,281 @@
|
|||
import { loadStripe, Stripe } from '@stripe/stripe-js';
|
||||
import { CUSTOM_ERROR } from 'utils/error';
|
||||
import { logError } from 'utils/sentry';
|
||||
import HTTPService from './HTTPService';
|
||||
|
||||
const getStripePublishableKey = (stripeAccount: StripeAccountCountry) => {
|
||||
if (stripeAccount === StripeAccountCountry.STRIPE_IN) {
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_STRIPE_IN_PUBLISHABLE_KEY ??
|
||||
'pk_live_51HAhqDK59oeucIMOiTI6MDDM2UWUbCAJXJCGsvjJhiO8nYJz38rQq5T4iyQLDMKxqEDUfU5Hopuj4U5U4dff23oT00fHvZeodC'
|
||||
);
|
||||
} else if (stripeAccount === StripeAccountCountry.STRIPE_US) {
|
||||
return (
|
||||
process.env.NEXT_PUBLIC_STRIPE_US_PUBLISHABLE_KEY ??
|
||||
'pk_live_51LZ9P4G1ITnQlpAnrP6pcS7NiuJo3SnJ7gibjJlMRatkrd2EY1zlMVTVQG5RkSpLPbsHQzFfnEtgHnk1PiylIFkk00tC0LWXwi'
|
||||
);
|
||||
} else {
|
||||
throw Error('stripe account not found');
|
||||
}
|
||||
};
|
||||
|
||||
const getEndpoint = () => {
|
||||
const endPoint =
|
||||
process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? 'https://api.ente.io';
|
||||
return endPoint;
|
||||
};
|
||||
enum PAYMENT_INTENT_STATUS {
|
||||
SUCCESS = 'success',
|
||||
REQUIRE_ACTION = 'requires_action',
|
||||
REQUIRE_PAYMENT_METHOD = 'requires_payment_method',
|
||||
}
|
||||
|
||||
enum FAILURE_REASON {
|
||||
// Unable to authenticate card or 3DS
|
||||
// User should be showing button for fixing card via customer portal
|
||||
AUTHENTICATION_FAILED = 'authentication_failed',
|
||||
// Card declined result in this error. Show button to the customer portal.
|
||||
REQUIRE_PAYMENT_METHOD = 'requires_payment_method',
|
||||
STRIPE_ERROR = 'stripe_error',
|
||||
CANCELED = 'canceled',
|
||||
SERVER_ERROR = 'server_error',
|
||||
}
|
||||
|
||||
enum STRIPE_ERROR_TYPE {
|
||||
CARD_ERROR = 'card_error',
|
||||
AUTHENTICATION_ERROR = 'authentication_error',
|
||||
}
|
||||
|
||||
enum STRIPE_ERROR_CODE {
|
||||
AUTHENTICATION_ERROR = 'payment_intent_authentication_failure',
|
||||
}
|
||||
|
||||
enum RESPONSE_STATUS {
|
||||
success = 'success',
|
||||
fail = 'fail',
|
||||
}
|
||||
|
||||
enum PaymentActionType {
|
||||
Buy = 'buy',
|
||||
Update = 'update',
|
||||
}
|
||||
|
||||
enum StripeAccountCountry {
|
||||
STRIPE_IN = 'IN',
|
||||
STRIPE_US = 'US',
|
||||
}
|
||||
|
||||
interface SubscriptionUpdateResponse {
|
||||
result: {
|
||||
status: PAYMENT_INTENT_STATUS;
|
||||
clientSecret: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function parseAndHandleRequest() {
|
||||
try {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const productID = urlParams.get('productID');
|
||||
const paymentToken = urlParams.get('paymentToken');
|
||||
const action = urlParams.get('action');
|
||||
const redirectURL = urlParams.get('redirectURL');
|
||||
if (!action && !paymentToken && !productID && !redirectURL) {
|
||||
throw Error(CUSTOM_ERROR.DIRECT_OPEN_WITH_NO_QUERY_PARAMS);
|
||||
} else if (!action || !paymentToken || !productID || !redirectURL) {
|
||||
throw Error(CUSTOM_ERROR.MISSING_REQUIRED_QUERY_PARAM);
|
||||
}
|
||||
switch (action) {
|
||||
case PaymentActionType.Buy:
|
||||
await buyPaidSubscription(productID, paymentToken, redirectURL);
|
||||
break;
|
||||
case PaymentActionType.Update:
|
||||
await updateSubscription(productID, paymentToken, redirectURL);
|
||||
break;
|
||||
default:
|
||||
throw Error(CUSTOM_ERROR.INVALID_ACTION);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Error: ", JSON.stringify(e)) ;
|
||||
if (e.message !== CUSTOM_ERROR.DIRECT_OPEN_WITH_NO_QUERY_PARAMS) {
|
||||
logError(e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserStripeAccountCountry(
|
||||
paymentToken: string,
|
||||
): Promise<{ stripeAccountCountry: StripeAccountCountry }> {
|
||||
const response = await HTTPService.get(
|
||||
`${getEndpoint()}/billing/stripe-account-country`,
|
||||
undefined,
|
||||
{
|
||||
'X-Auth-Token': paymentToken,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function getStripe(
|
||||
redirectURL: string,
|
||||
stripeAccount: StripeAccountCountry,
|
||||
) {
|
||||
try {
|
||||
const publishableKey = getStripePublishableKey(stripeAccount);
|
||||
const stripe = await loadStripe(publishableKey);
|
||||
|
||||
if (!stripe) {
|
||||
throw Error('stripe load failed');
|
||||
}
|
||||
return stripe;
|
||||
} catch (e) {
|
||||
logError(e, 'stripe load failed');
|
||||
redirectToApp(
|
||||
redirectURL,
|
||||
RESPONSE_STATUS.fail,
|
||||
FAILURE_REASON.STRIPE_ERROR,
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function buyPaidSubscription(
|
||||
productID: string,
|
||||
paymentToken: string,
|
||||
redirectURL: string,
|
||||
) {
|
||||
try {
|
||||
const { stripeAccountCountry } = await getUserStripeAccountCountry(
|
||||
paymentToken,
|
||||
);
|
||||
const stripe = await getStripe(redirectURL, stripeAccountCountry);
|
||||
const { sessionID } = await createCheckoutSession(
|
||||
productID,
|
||||
paymentToken,
|
||||
redirectURL,
|
||||
);
|
||||
await stripe.redirectToCheckout({
|
||||
sessionId: sessionID,
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e, 'subscription purchase failed');
|
||||
redirectToApp(
|
||||
redirectURL,
|
||||
RESPONSE_STATUS.fail,
|
||||
FAILURE_REASON.SERVER_ERROR,
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function createCheckoutSession(
|
||||
productID: string,
|
||||
paymentToken: string,
|
||||
redirectURL: string,
|
||||
): Promise<{ sessionID: string }> {
|
||||
const response = await HTTPService.get(
|
||||
`${getEndpoint()}/billing/stripe/checkout-session`,
|
||||
{
|
||||
productID,
|
||||
redirectURL,
|
||||
},
|
||||
{
|
||||
'X-Auth-Token': paymentToken,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateSubscription(
|
||||
productID: string,
|
||||
paymentToken: string,
|
||||
redirectURL: string,
|
||||
) {
|
||||
try {
|
||||
const { stripeAccountCountry } = await getUserStripeAccountCountry(
|
||||
paymentToken,
|
||||
);
|
||||
const stripe = await getStripe(redirectURL, stripeAccountCountry);
|
||||
const { result } = await subscriptionUpdateRequest(
|
||||
paymentToken,
|
||||
productID,
|
||||
);
|
||||
switch (result.status) {
|
||||
case PAYMENT_INTENT_STATUS.SUCCESS:
|
||||
// subscription updated successfully
|
||||
// no-op required
|
||||
return redirectToApp(redirectURL, RESPONSE_STATUS.success);
|
||||
|
||||
case PAYMENT_INTENT_STATUS.REQUIRE_PAYMENT_METHOD:
|
||||
return redirectToApp(
|
||||
redirectURL,
|
||||
RESPONSE_STATUS.fail,
|
||||
FAILURE_REASON.REQUIRE_PAYMENT_METHOD,
|
||||
);
|
||||
case PAYMENT_INTENT_STATUS.REQUIRE_ACTION: {
|
||||
const { error } = await stripe.confirmCardPayment(
|
||||
result.clientSecret,
|
||||
);
|
||||
if (error) {
|
||||
logError(
|
||||
error,
|
||||
`${error.message} - subscription update failed`,
|
||||
);
|
||||
if (error.type === STRIPE_ERROR_TYPE.CARD_ERROR) {
|
||||
return redirectToApp(
|
||||
redirectURL,
|
||||
RESPONSE_STATUS.fail,
|
||||
FAILURE_REASON.REQUIRE_PAYMENT_METHOD,
|
||||
);
|
||||
} else if (
|
||||
error.type === STRIPE_ERROR_TYPE.AUTHENTICATION_ERROR ||
|
||||
error.code === STRIPE_ERROR_CODE.AUTHENTICATION_ERROR
|
||||
) {
|
||||
return redirectToApp(
|
||||
redirectURL,
|
||||
RESPONSE_STATUS.fail,
|
||||
FAILURE_REASON.AUTHENTICATION_FAILED,
|
||||
);
|
||||
} else {
|
||||
return redirectToApp(redirectURL, RESPONSE_STATUS.fail);
|
||||
}
|
||||
} else {
|
||||
return redirectToApp(redirectURL, RESPONSE_STATUS.success);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'subscription update failed');
|
||||
redirectToApp(
|
||||
redirectURL,
|
||||
RESPONSE_STATUS.fail,
|
||||
FAILURE_REASON.SERVER_ERROR,
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async function subscriptionUpdateRequest(
|
||||
paymentToken: string,
|
||||
productID: string,
|
||||
): Promise<SubscriptionUpdateResponse> {
|
||||
const response = await HTTPService.post(
|
||||
`${getEndpoint()}/billing/stripe/update-subscription`,
|
||||
{
|
||||
productID,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
'X-Auth-Token': paymentToken,
|
||||
},
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function redirectToApp(redirectURL: string, status: string, reason?: string) {
|
||||
let completePath = `${redirectURL}?status=${status}`;
|
||||
if (reason) {
|
||||
completePath = `${completePath}&reason=${reason}`;
|
||||
}
|
||||
window.location.href = completePath;
|
||||
}
|
41
web/apps/payments/src/styles/globals.css
Normal file
41
web/apps/payments/src/styles/globals.css
Normal file
|
@ -0,0 +1,41 @@
|
|||
/* ubuntu-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(''), url('/fonts/ubuntu-v15-latin-regular.woff2') format('woff2'),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/fonts/ubuntu-v15-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* ubuntu-700 - latin */
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local(''), url('/fonts/ubuntu-v15-latin-700.woff2') format('woff2'),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/fonts/ubuntu-v15-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #191919 !important;
|
||||
color: #aaa !important;
|
||||
font-family: Ubuntu, Arial, sans-serif !important;
|
||||
}
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
color: #d7d7d7;
|
||||
}
|
||||
|
||||
#__next {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
3
web/apps/payments/src/utils/common.ts
Normal file
3
web/apps/payments/src/utils/common.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function runningInBrowser() {
|
||||
return typeof window !== 'undefined';
|
||||
}
|
5
web/apps/payments/src/utils/error/index.ts
Normal file
5
web/apps/payments/src/utils/error/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const CUSTOM_ERROR = {
|
||||
DIRECT_OPEN_WITH_NO_QUERY_PARAMS: 'direct open with no query params',
|
||||
MISSING_REQUIRED_QUERY_PARAM: 'missing required query param',
|
||||
INVALID_ACTION: 'invalid action',
|
||||
};
|
28
web/apps/payments/src/utils/localStorage/index.ts
Normal file
28
web/apps/payments/src/utils/localStorage/index.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { logError } from 'utils/sentry';
|
||||
|
||||
export enum LS_KEYS {
|
||||
AnonymizeUserID = 'anonymizedUserID',
|
||||
}
|
||||
|
||||
export const getData = (key: LS_KEYS) => {
|
||||
try {
|
||||
if (
|
||||
typeof localStorage === 'undefined' ||
|
||||
typeof key === 'undefined' ||
|
||||
typeof localStorage.getItem(key) === 'undefined'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const data = localStorage.getItem(key);
|
||||
return data && JSON.parse(data);
|
||||
} catch (e) {
|
||||
logError(e, 'Failed to Parse JSON');
|
||||
}
|
||||
};
|
||||
|
||||
export const setData = (key: LS_KEYS, value: object) => {
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
};
|
14
web/apps/payments/src/utils/sentry/index.ts
Normal file
14
web/apps/payments/src/utils/sentry/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import * as Sentry from '@sentry/nextjs';
|
||||
import { getUserAnonymizedID } from 'utils/user';
|
||||
|
||||
export const logError = (e: any, msg?: string) => {
|
||||
Sentry.captureException(e, {
|
||||
level: "info",
|
||||
user: { id: getUserAnonymizedID() },
|
||||
contexts: {
|
||||
context: {
|
||||
message: msg,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
4
web/apps/payments/src/utils/strings/constants.ts
Normal file
4
web/apps/payments/src/utils/strings/constants.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { getConstantValue } from './vernacularStrings';
|
||||
|
||||
const constants = getConstantValue();
|
||||
export default constants;
|
6
web/apps/payments/src/utils/strings/englishConstants.tsx
Normal file
6
web/apps/payments/src/utils/strings/englishConstants.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
const englishConstants = {
|
||||
TITLE: 'Payments | ente.io',
|
||||
SOMETHING_WENT_WRONG: 'Oops, something went wrong.',
|
||||
NOT_FOUND: '404 | This page could not be found.',
|
||||
};
|
||||
export default englishConstants;
|
87
web/apps/payments/src/utils/strings/vernacularStrings.ts
Normal file
87
web/apps/payments/src/utils/strings/vernacularStrings.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { runningInBrowser } from 'utils/common';
|
||||
import englishConstants from './englishConstants';
|
||||
|
||||
/** Enums of supported locale */
|
||||
export enum locale {
|
||||
en = 'en',
|
||||
hi = 'hi',
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a template with placeholders which can then be
|
||||
* substituted at run time. Enabling the developer to create
|
||||
* different template for different locale and populate them
|
||||
* at run time.
|
||||
*
|
||||
* @param strings
|
||||
* @param keys
|
||||
* @returns string
|
||||
*/
|
||||
export function template(
|
||||
strings: TemplateStringsArray,
|
||||
...keys: string[] | number[]
|
||||
) {
|
||||
return (...values: any[]) => {
|
||||
const dict = values[values.length - 1] || {};
|
||||
const result = [strings[0]];
|
||||
keys.forEach((key, i) => {
|
||||
const value = typeof key === 'number' ? values[key] : dict[key];
|
||||
result.push(value, strings[i + 1]);
|
||||
});
|
||||
return result.join('');
|
||||
};
|
||||
}
|
||||
|
||||
/** Type for vernacular string constants */
|
||||
export type VernacularConstants<T> = {
|
||||
[locale.en]: T;
|
||||
[locale.hi]?: {
|
||||
[x in keyof T]?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a valid locale from string and defaults
|
||||
* to English.
|
||||
*
|
||||
* @param lang
|
||||
*/
|
||||
export const getLocale = (lang: string) => {
|
||||
switch (lang) {
|
||||
case locale.hi:
|
||||
return locale.hi;
|
||||
default:
|
||||
return locale.en;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Global constants
|
||||
*/
|
||||
const globalConstants: VernacularConstants<typeof englishConstants> = {
|
||||
en: englishConstants,
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to extend global constants with local constants
|
||||
* @param localConstants
|
||||
*/
|
||||
export function getConstantValue<T>(localConstants?: VernacularConstants<T>) {
|
||||
const searchParam = runningInBrowser() ? window.location.search : '';
|
||||
const query = new URLSearchParams(searchParam);
|
||||
const currLocale = getLocale(query.get('lang') ?? 'en');
|
||||
|
||||
if (currLocale !== 'en') {
|
||||
return {
|
||||
...globalConstants.en,
|
||||
...localConstants?.en,
|
||||
...globalConstants[currLocale],
|
||||
...localConstants?.[currLocale],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...globalConstants[currLocale],
|
||||
...localConstants?.[currLocale],
|
||||
};
|
||||
}
|
23
web/apps/payments/src/utils/user/index.ts
Normal file
23
web/apps/payments/src/utils/user/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { getData, LS_KEYS, setData } from 'utils/localStorage';
|
||||
|
||||
export function makeID(length: number) {
|
||||
let result = '';
|
||||
const characters =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
const charactersLength = characters.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(
|
||||
Math.floor(Math.random() * charactersLength),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getUserAnonymizedID() {
|
||||
let anonymizeUserID = getData(LS_KEYS.AnonymizeUserID)?.id;
|
||||
if (!anonymizeUserID) {
|
||||
anonymizeUserID = makeID(6);
|
||||
setData(LS_KEYS.AnonymizeUserID, { id: anonymizeUserID });
|
||||
}
|
||||
return anonymizeUserID;
|
||||
}
|
31
web/apps/payments/tsconfig.json
Normal file
31
web/apps/payments/tsconfig.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": "./src",
|
||||
"incremental": true
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
3364
web/apps/payments/yarn.lock
Normal file
3364
web/apps/payments/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue