├── .env.template
├── .github
└── workflows
│ ├── cd-to-share.yml
│ ├── cd.yml
│ └── stale-prs.yml
├── .gitignore
├── .prettierrc.json
├── CODEOWNERS
├── LICENSE
├── README.md
├── infrastructure
└── share
│ ├── share.Dockerfile
│ └── share.env
├── package.json
├── postcss.config.js
├── public
├── Drive-1.webp
├── Photos-2.webp
├── bg.png
├── byemail.png
├── bylink.png
├── favicon.ico
├── images
│ └── bg.png
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── whitepaper
│ └── internxt-white-paper.pdf
├── src
├── App.tsx
├── Layout.tsx
├── assets
│ ├── Internxt.svg
│ ├── images
│ │ ├── HeroSectionImages
│ │ │ ├── Blog.svg
│ │ │ ├── Blog.webp
│ │ │ ├── Pricing.svg
│ │ │ ├── Pricing.webp
│ │ │ ├── Privacy.svg
│ │ │ └── Privacy.webp
│ │ ├── footer
│ │ │ ├── app-store.svg
│ │ │ └── store-for-android.svg
│ │ └── share-file
│ │ │ ├── banner-bg.png
│ │ │ └── visual.svg
│ ├── lang
│ │ └── en
│ │ │ ├── footer.json
│ │ │ ├── navbar.json
│ │ │ └── send.json
│ └── social
│ │ ├── black
│ │ ├── facebook.svg
│ │ ├── instagram.svg
│ │ ├── linkedin.svg
│ │ └── twitter.svg
│ │ ├── cool-gray-30
│ │ ├── facebook.svg
│ │ ├── instagram.svg
│ │ ├── linkedin.svg
│ │ ├── mastodon.svg
│ │ ├── twitter.svg
│ │ └── youtube.svg
│ │ ├── cool-gray-60
│ │ ├── facebook.svg
│ │ ├── instagram.svg
│ │ ├── linkedin.svg
│ │ ├── mastodon.svg
│ │ ├── twitter.svg
│ │ └── youtube.svg
│ │ ├── gdpr-internxt.svg
│ │ ├── neutral-300
│ │ ├── facebook.svg
│ │ ├── instagram.svg
│ │ ├── linkedin.svg
│ │ └── twitter.svg
│ │ ├── reddit.svg
│ │ └── white
│ │ ├── facebook.svg
│ │ ├── instagram.svg
│ │ ├── linkedin.svg
│ │ └── twitter.svg
├── components
│ ├── Button.tsx
│ ├── Card.tsx
│ ├── CardBotton.tsx
│ ├── Dropdown.tsx
│ ├── FancySpinner.tsx
│ ├── FaqAccordion.tsx
│ ├── FileArea.tsx
│ ├── Input.tsx
│ ├── ItemList.tsx
│ ├── Navbar
│ │ └── Navbar.tsx
│ ├── NotificationToast.tsx
│ ├── RevealX.tsx
│ ├── RevealY.tsx
│ ├── RootDropzone.tsx
│ ├── SendBanner.tsx
│ ├── Spinner.tsx
│ ├── Switch.tsx
│ ├── Tooltip.tsx
│ ├── footer
│ │ ├── Footer.module.scss
│ │ └── Footer.tsx
│ ├── send
│ │ ├── CtaSection.tsx
│ │ ├── FaqSection.tsx
│ │ ├── FeatureSection.tsx
│ │ └── InfoSection.tsx
│ └── utils
│ │ └── schema-markup-generator.js
├── constants.ts
├── contexts
│ └── Files.tsx
├── index.tsx
├── lib
│ ├── auth.ts
│ ├── stringUtils.test.ts
│ ├── stringUtils.ts
│ └── urls.ts
├── logo.svg
├── logo_dark.svg
├── models
│ └── SendItem.ts
├── network
│ ├── NetworkFacade.ts
│ ├── crypto.ts
│ ├── download.ts
│ ├── requests.ts
│ ├── streams.ts
│ └── upload.ts
├── react-app-env.d.ts
├── reportWebVitals.ts
├── services
│ ├── analytics.service.ts
│ ├── download.service.ts
│ ├── error-reporting.service.ts
│ ├── items.service.ts
│ ├── network.service.ts
│ ├── notifications.service.ts
│ ├── stream.service.ts
│ ├── upload.service.test.ts
│ ├── upload.service.ts
│ └── zip
│ │ ├── FlatFolderZip.ts
│ │ └── Zip.ts
├── setupTests.ts
├── styles
│ ├── fonts
│ │ └── InstrumentSans
│ │ │ ├── InstrumentSans-Bold.woff2
│ │ │ ├── InstrumentSans-Medium.woff2
│ │ │ ├── InstrumentSans-Regular.woff2
│ │ │ ├── InstrumentSans-SemiBold.woff2
│ │ │ ├── InstrumentSans[wdth,wght].woff2
│ │ │ ├── OFL.txt
│ │ │ └── font.css
│ └── index.css
└── views
│ ├── DownloadView.tsx
│ ├── HomeView.tsx
│ └── NotFoundView.tsx
├── tailwind.config.js
├── tsconfig.json
├── vitest.config.js
└── yarn.lock
/.env.template:
--------------------------------------------------------------------------------
1 | BROWSER=none
2 | PORT=5000
3 |
4 | REACT_APP_SEND_USER=
5 | REACT_APP_SEND_PASS=
6 | REACT_APP_SEND_ENCRYPTION_KEY=
7 | REACT_APP_SEND_BUCKET_ID=
8 | REACT_APP_SEND_API_URL=http://drive-server-wip:3004/api
9 |
10 | REACT_APP_NETWORK_URL=http://network-api:6382
11 | REACT_APP_PROXY=""
12 | REACT_APP_DONT_USE_PROXY=false
13 | REACT_APP_SENTRY_DSN=
14 | REACT_APP_RECAPTCHA_V3=
15 | REACT_APP_GA_ID=
16 |
--------------------------------------------------------------------------------
/.github/workflows/cd-to-share.yml:
--------------------------------------------------------------------------------
1 | name: CD to share.internxt.com
2 | on:
3 | push:
4 | branches: [master, feat/share-domain]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - run: echo ${{ secrets.SHARE_NGINX_CONFIG }} | base64 -d > ./infrastructure/share/nginx.conf
11 |
12 | - run: echo REACT_APP_SEGMENT_KEY=${{ secrets.REACT_APP_SEGMENT_KEY }} >> ./infrastructure/share/share.env
13 | - run: echo REACT_APP_CRYPTO_SECRET=${{ secrets.REACT_APP_CRYPTO_SECRET }} >> ./infrastructure/share/share.env
14 | - run: echo REACT_APP_STRIPE_PK=${{ secrets.REACT_APP_STRIPE_PK }} >> ./infrastructure/share/share.env
15 | - run: echo REACT_APP_STRIPE_TEST_PK=${{ secrets.REACT_APP_STRIPE_TEST_PK }} >> ./infrastructure/share/share.env
16 | - run: echo REACT_APP_SEND_API_URL=${{ secrets.REACT_APP_SEND_API_URL }} >> ./infrastructure/share/share.env
17 | - run: echo GENERATE_SOURCEMAP=${{ secrets.GENERATE_SOURCEMAP }} >> ./infrastructure/share/share.env
18 | - run: echo REACT_APP_MAGIC_IV=${{ secrets.REACT_APP_MAGIC_IV }} >> ./infrastructure/share/share.env
19 | - run: echo REACT_APP_MAGIC_SALT=${{ secrets.REACT_APP_MAGIC_SALT }} >> ./infrastructure/share/share.env
20 | - run: echo REACT_APP_CRYPTO_SECRET2=${{ secrets.REACT_APP_CRYPTO_SECRET2 }} >> ./infrastructure/share/share.env
21 | - run: echo REACT_APP_PROXY=${{ secrets.REACT_APP_PROXY }} >> ./infrastructure/share/share.env
22 | - run: echo REACT_APP_NETWORK_URL=${{ secrets.REACT_APP_NETWORK_URL }} >> ./infrastructure/share/share.env
23 | - run: echo REACT_APP_SEND_USER=${{ secrets.REACT_APP_SEND_USER }} >> ./infrastructure/share/share.env
24 | - run: echo REACT_APP_SEND_PASS=${{ secrets.REACT_APP_SEND_PASS }} >> ./infrastructure/share/share.env
25 | - run: echo REACT_APP_SEND_ENCRYPTION_KEY=${{ secrets.REACT_APP_SEND_ENCRYPTION_KEY }} >> ./infrastructure/share/share.env
26 | - run: echo REACT_APP_SEND_BUCKET_ID=${{ secrets.REACT_APP_SEND_BUCKET_ID }} >> ./infrastructure/share/share.env
27 | - run: echo REACT_APP_SENTRY_DSN=${{ secrets.REACT_APP_SENTRY_DSN }} >> ./infrastructure/share/share.env
28 |
29 | - run: echo "registry=https://registry.yarnpkg.com/" > .npmrc
30 | - run: echo "@internxt:registry=https://npm.pkg.github.com" >> .npmrc
31 | # You cannot read packages from other private repos with GITHUB_TOKEN
32 | # You have to use a PAT instead https://github.com/actions/setup-node/issues/49
33 | - run: echo //npm.pkg.github.com/:_authToken=${{ secrets.PERSONAL_ACCESS_TOKEN }} >> .npmrc
34 | - run: echo "always-auth=true" >> .npmrc
35 |
36 | - name: Login to DockerHub
37 | uses: docker/login-action@v1
38 | with:
39 | username: ${{ secrets.DOCKERHUB_USERNAME }}
40 | password: ${{ secrets.DOCKERHUB_TOKEN }}
41 | - name: Set up Docker Buildx
42 | uses: docker/setup-buildx-action@v1
43 | - name: Build and push to send-web
44 | uses: docker/build-push-action@v2
45 | with:
46 | context: ./
47 | file: ./infrastructure/share/share.Dockerfile
48 | push: true
49 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/send-web:${{ github.sha }}
50 | deploy:
51 | needs: build
52 | runs-on: ubuntu-latest
53 | steps:
54 | - uses: actions/checkout@master
55 | - name: Updates drive-web cluster image
56 | uses: steebchen/kubectl@v2.0.0
57 | with: # defaults to latest kubectl binary version
58 | config: ${{ secrets.KUBE_CONFIG_DRIVE_SERVER }}
59 | command: set image --record deployment/send-web-share-dp send-web-share=${{ secrets.DOCKERHUB_USERNAME }}/send-web:${{ github.sha }} -n drive
60 | - name: Verify deployment
61 | uses: steebchen/kubectl@v2.0.0
62 | with:
63 | config: ${{ secrets.KUBE_CONFIG_DRIVE_SERVER }}
64 | version: v1.20.2 # specify kubectl binary version explicitly
65 | command: rollout status deployment/send-web-share-dp
66 |
--------------------------------------------------------------------------------
/.github/workflows/cd.yml:
--------------------------------------------------------------------------------
1 | name: Send Web CD
2 | on:
3 | push:
4 | branches: [master, feat/cd]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | permissions:
9 | contents: read
10 | packages: read
11 | strategy:
12 | matrix:
13 | node-version: [16.x]
14 | steps:
15 | - name: Checkout repo
16 | uses: actions/checkout@v4
17 | - name: Use Node.js ${{ matrix.node-version }}
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: ${{ matrix.node-version }}
21 | registry-url: "https://npm.pkg.github.com"
22 | - run: echo REACT_APP_SEND_USER=${{ secrets.REACT_APP_SEND_USER }} >> ./.env
23 | - run: echo REACT_APP_SEND_PASS=${{ secrets.REACT_APP_SEND_PASS }} >> ./.env
24 | - run: echo REACT_APP_SEGMENT_KEY=${{ secrets.REACT_APP_SEGMENT_KEY }} >> ./.env
25 | - run: echo REACT_APP_GA_ID=${{ secrets.REACT_APP_GA_ID }} >> ./.env
26 | - run: echo REACT_APP_CRYPTO_SECRET=${{ secrets.REACT_APP_CRYPTO_SECRET }} >> ./.env
27 | - run: echo REACT_APP_STRIPE_PK=${{ secrets.REACT_APP_STRIPE_PK }} >> ./.env
28 | - run: echo REACT_APP_STRIPE_TEST_PK=${{ secrets.REACT_APP_STRIPE_TEST_PK }} >> ./.env
29 | - run: echo REACT_APP_SEND_API_URL=${{ secrets.REACT_APP_SEND_API_URL }} >> ./.env
30 | - run: echo REACT_APP_MAGIC_IV=${{ secrets.REACT_APP_MAGIC_IV }} >> ./.env
31 | - run: echo REACT_APP_MAGIC_SALT=${{ secrets.REACT_APP_MAGIC_SALT }} >> ./.env
32 | - run: echo REACT_APP_CRYPTO_SECRET2=${{ secrets.REACT_APP_CRYPTO_SECRET2 }} >> ./.env
33 | - run: echo REACT_APP_PROXY=${{ secrets.REACT_APP_PROXY }} >> ./.env
34 | - run: echo REACT_APP_DONT_USE_PROXY=${{ secrets.REACT_APP_DONT_USE_PROXY }} >> ./.env
35 | - run: echo REACT_APP_NETWORK_URL=${{ secrets.REACT_APP_NETWORK_URL }} >> ./.env
36 | - run: echo REACT_APP_RECAPTCHA_V3=${{ secrets.REACT_APP_RECAPTCHA_V3 }} >> ./.env
37 | - run: echo REACT_APP_SEND_ENCRYPTION_KEY=${{ secrets.REACT_APP_SEND_ENCRYPTION_KEY }} >> ./.env
38 | - run: echo REACT_APP_SEND_BUCKET_ID=${{ secrets.REACT_APP_SEND_BUCKET_ID }} >> ./.env
39 | - run: echo REACT_APP_SENTRY_DSN=${{ secrets.REACT_APP_SENTRY_DSN }} >> ./.env
40 | - run: echo REACT_APP_NODE_ENV=production >> ./.env
41 | - run: echo "registry=https://registry.yarnpkg.com/" > .npmrc
42 | - run: echo "@internxt:registry=https://npm.pkg.github.com" >> .npmrc
43 | # You cannot read packages from other private repos with GITHUB_TOKEN
44 | # You have to use a PAT instead https://github.com/actions/setup-node/issues/49
45 | - run: echo //npm.pkg.github.com/:_authToken=${{ secrets.PERSONAL_ACCESS_TOKEN }} >> .npmrc
46 | - run: echo "always-auth=true" >> .npmrc
47 |
48 | # Setup node
49 | - name: Use Node.js ${{ matrix.node-version }}
50 | uses: actions/setup-node@v4
51 | with:
52 | node-version: ${{ matrix.node-version }}
53 |
54 | # Setup dependencies
55 | - run: yarn
56 | env:
57 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58 |
59 | # Build
60 | - run: yarn run build
61 | env:
62 | CI: false
63 |
64 | # Upload build directory as an artifact
65 | - name: Upload build directory
66 | uses: actions/upload-artifact@v3
67 | with:
68 | name: build
69 | path: build/
70 |
71 | publish:
72 | needs: build
73 | runs-on: ubuntu-latest
74 | permissions:
75 | contents: read
76 | deployments: write
77 | name: Publish to Cloudflare Pages
78 | steps:
79 | - name: Checkout repo
80 | uses: actions/checkout@v4
81 |
82 | # Download the build artifact
83 | - name: Download build artifact
84 | uses: actions/download-artifact@v3
85 | with:
86 | name: build
87 | path: build
88 |
89 | - name: Publish to Cloudflare Pages
90 | uses: cloudflare/pages-action@v1
91 | with:
92 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
93 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
94 | projectName: send-web
95 | directory: build
96 | # Optional: Enable this if you want to have GitHub Deployments triggered
97 | # gitHubToken: ${{ secrets.GITHUB_TOKEN }}
98 | # Optional: Switch what branch you are publishing to.
99 | # By default this will be the branch which triggered this workflow
100 | branch: main
101 | # Optional: Change the working directory
102 | # workingDirectory: my-site
103 | # Optional: Change the Wrangler version, allows you to point to a specific version or a tag such as `beta`
104 | wranglerVersion: "3"
105 |
--------------------------------------------------------------------------------
/.github/workflows/stale-prs.yml:
--------------------------------------------------------------------------------
1 | name: Mark stale PRs
2 |
3 | on:
4 | schedule:
5 | - cron: "0 7 * * *"
6 | workflow_dispatch:
7 |
8 | permissions:
9 | issues: write
10 | pull-requests: write
11 |
12 | jobs:
13 | stale:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/stale@v9
18 | with:
19 | repo-token: ${{ secrets.GITHUB_TOKEN }}
20 | days-before-stale: 30
21 | days-before-close: 15
22 | # Custom messages
23 | stale-pr-message: >
24 | This pull request has been inactive for 30 days. Is it still in progress?
25 | If so, please leave a comment or make an update to keep it open. Otherwise, it will be closed automatically in 15 days.
26 | close-pr-message: >
27 | This pull request was closed automatically due to prolonged inactivity.
28 | # Labels
29 | stale-pr-label: stalled
30 | exempt-pr-labels: "dependencies,blocked"
31 | exempt-draft-pr: true
32 | ascending: true
33 | # Do not touch issues
34 | days-before-issue-stale: -1
35 | days-before-issue-close: -1
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
106 |
107 | # dependencies
108 | /node_modules
109 | /.pnp
110 | .pnp.js
111 |
112 | # testing
113 | /coverage
114 |
115 | # production
116 | /build
117 |
118 | # misc
119 | .DS_Store
120 | .env.local
121 | .env.development.local
122 | .env.test.local
123 | .env.production.local
124 |
125 | npm-debug.log*
126 | yarn-debug.log*
127 | yarn-error.log*
128 |
129 | .npmrc
130 | index.generated.css
131 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @xabg2 @larryrider @CandelR
2 |
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Internxt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
13 |
14 | The page will reload if you make edits.\
15 | You will also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
--------------------------------------------------------------------------------
/infrastructure/share/share.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16-alpine3.12
2 |
3 | RUN apk update
4 | RUN apk add nginx git
5 |
6 | WORKDIR /app
7 | COPY package.json yarn.lock ./
8 | COPY .npmrc /app/.npmrc
9 |
10 | RUN yarn
11 | COPY . /app
12 | COPY ./infrastructure/share/share.env /app/.env
13 | RUN yarn build
14 |
15 | RUN mkdir -p /run/nginx
16 | COPY ./infrastructure/share/nginx.conf /etc/nginx/nginx.conf
17 |
18 | EXPOSE 80
19 |
20 | CMD ["nginx","-g","daemon off;"]
21 |
--------------------------------------------------------------------------------
/infrastructure/share/share.env:
--------------------------------------------------------------------------------
1 | # ----These can be public ----
2 | REACT_APP_API_URL=https://send.internxt.com
3 | GENERATE_SOURCEMAP=false
4 | REACT_APP_NETWORK_URL=https://api.internxt.com
5 | PUBLIC_URL=https://share.internxt.com/s
6 | REACT_APP_BASE_URL=/s
7 |
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "send-web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@headlessui/react": "^1.6.6",
7 | "@internxt/inxt-js": "=1.2.21",
8 | "@internxt/lib": "^1.2.0",
9 | "@internxt/sdk": "^0.15.5",
10 | "@sentry/react": "^7.3.1",
11 | "@sentry/tracing": "^7.3.1",
12 | "@testing-library/jest-dom": "^5.16.4",
13 | "@testing-library/react": "^13.3.0",
14 | "@testing-library/user-event": "^13.5.0",
15 | "@types/jest": "^27.5.2",
16 | "@types/node": "^16.11.41",
17 | "@types/react": "^18.0.21",
18 | "@types/react-dom": "^18.0.5",
19 | "@types/streamsaver": "^2.0.1",
20 | "@types/uuid": "^8.3.4",
21 | "asmcrypto.js": "^2.3.2",
22 | "async": "^3.2.4",
23 | "axios": "^0.27.2",
24 | "bip39": "^3.0.4",
25 | "bytes": "^3.1.2",
26 | "copy-to-clipboard": "^3.3.1",
27 | "hamburger-react": "^2.5.0",
28 | "js-file-download": "^0.4.12",
29 | "lodash.throttle": "^4.1.1",
30 | "moment": "^2.29.4",
31 | "phosphor-react": "^1.4.1",
32 | "react": "^18.2.0",
33 | "react-dom": "^18.2.0",
34 | "react-dropzone": "14.2.1",
35 | "react-hot-toast": "^2.2.0",
36 | "react-markdown": "^8.0.3",
37 | "react-router-dom": "6",
38 | "react-scripts": "4.0.3",
39 | "streamsaver": "^2.0.6",
40 | "typescript": "^4.7.4",
41 | "uuid": "^9.0.0",
42 | "web-vitals": "^2.1.4"
43 | },
44 | "scripts": {
45 | "start": "concurrently \"yarn start:css\" \"react-scripts start\"",
46 | "build": "yarn run build:css && react-scripts build",
47 | "test": "vitest run src/",
48 | "eject": "react-scripts eject",
49 | "start:css": "tailwindcss -w -i ./src/styles/index.css -o ./src/styles/index.generated.css",
50 | "build:css": "tailwindcss -m -i ./src/styles/index.css -o ./src/styles/index.generated.css",
51 | "add:npmrc": "echo \"registry=https://registry.yarnpkg.com/\" >> .npmrc && echo \"@internxt:registry=https://npm.pkg.github.com\" >> .npmrc && echo \"//npm.pkg.github.com/:_authToken=$NPM_TOKEN\" >> .npmrc && echo \"always-auth=true\" >> .npmrc",
52 | "vercel:install": "yarn run add:npmrc && yarn install --ignore-engines"
53 | },
54 | "eslintConfig": {
55 | "extends": [
56 | "react-app",
57 | "react-app/jest"
58 | ]
59 | },
60 | "browserslist": {
61 | "production": [
62 | ">0.2%",
63 | "not dead",
64 | "not op_mini all"
65 | ],
66 | "development": [
67 | "last 1 chrome version",
68 | "last 1 firefox version",
69 | "last 1 safari version"
70 | ]
71 | },
72 | "devDependencies": {
73 | "@types/async": "^3.2.13",
74 | "@types/bytes": "^3.1.1",
75 | "@types/lodash.throttle": "^4.1.7",
76 | "@types/mime-types": "^2.1.1",
77 | "@vitejs/plugin-react": "^4.3.4",
78 | "autoprefixer": "^10.4.7",
79 | "concurrently": "^7.2.2",
80 | "postcss": "^8.4.14",
81 | "postcss-cli": "^9.1.0",
82 | "prettier": "^2.7.1",
83 | "prettier-plugin-tailwindcss": "^0.1.11",
84 | "tailwindcss": "^3.1.4",
85 | "vitest": "^2.1.8"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/Drive-1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/public/Drive-1.webp
--------------------------------------------------------------------------------
/public/Photos-2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/public/Photos-2.webp
--------------------------------------------------------------------------------
/public/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/public/bg.png
--------------------------------------------------------------------------------
/public/byemail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/public/byemail.png
--------------------------------------------------------------------------------
/public/bylink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/public/bylink.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/public/images/bg.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
18 |
19 |
28 |
29 |
30 | Internxt Send – Securely Send Large Files for Free
31 |
78 |
79 |
80 |
90 |
91 |
92 |
93 |
94 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow: /download/*
--------------------------------------------------------------------------------
/public/whitepaper/internxt-white-paper.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/public/whitepaper/internxt-white-paper.pdf
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster } from "react-hot-toast";
2 | import { BrowserRouter, Navigate, Route, Routes, useParams, useSearchParams } from "react-router-dom";
3 | import RootDropzone from "./components/RootDropzone";
4 | import { FilesProvider } from "./contexts/Files";
5 | import DownloadView from "./views/DownloadView";
6 | import HomeView from "./views/HomeView";
7 | import NotFoundView from "./views/NotFoundView";
8 |
9 | const browserRouterConfig: { basename?: string } = {};
10 | if (process.env.REACT_APP_BASE_URL) {
11 | browserRouterConfig.basename = process.env.REACT_APP_BASE_URL;
12 | }
13 |
14 | function DownloadRedirectWrapper() {
15 | const { sendId } = useParams();
16 | const [searchParams] = useSearchParams();
17 | const code = searchParams.get("code") ?? '';
18 |
19 | return ;
20 | }
21 |
22 | function App() {
23 | return (
24 | <>
25 |
26 |
27 |
31 |
32 |
33 |
34 |
35 | }
36 | />
37 | } />
38 | } />
39 | } />
40 |
41 |
42 |
43 | >
44 | );
45 | }
46 |
47 | export default App;
48 |
--------------------------------------------------------------------------------
/src/assets/Internxt.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/assets/images/HeroSectionImages/Blog.svg:
--------------------------------------------------------------------------------
1 |
26 |
--------------------------------------------------------------------------------
/src/assets/images/HeroSectionImages/Blog.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/src/assets/images/HeroSectionImages/Blog.webp
--------------------------------------------------------------------------------
/src/assets/images/HeroSectionImages/Pricing.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/src/assets/images/HeroSectionImages/Pricing.webp
--------------------------------------------------------------------------------
/src/assets/images/HeroSectionImages/Privacy.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/src/assets/images/HeroSectionImages/Privacy.webp
--------------------------------------------------------------------------------
/src/assets/images/footer/app-store.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/footer/store-for-android.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/images/share-file/banner-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/src/assets/images/share-file/banner-bg.png
--------------------------------------------------------------------------------
/src/assets/images/share-file/visual.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
--------------------------------------------------------------------------------
/src/assets/lang/en/footer.json:
--------------------------------------------------------------------------------
1 | {
2 | "DownloadApp": {
3 | "title": "Download Internxt",
4 | "description": "Seamless file management and secure cloud storage on the go with the Internxt mobile app for iOS or Android."
5 | },
6 | "NewsletterSection": {
7 | "title": "Newsletter",
8 | "description": "Want to hear from us when we launch new products or release a new update? Type your mail address and we’ll do the rest!",
9 | "input": "Your email address",
10 | "info": "You can unsubscribe at any time",
11 | "cta": "Subscribe",
12 | "privacy": "By subscribing you agree to our",
13 | "privacyLink": "privacy policy."
14 | },
15 | "FooterSection": {
16 | "description": "Internxt’s mission is to help shape a world that respects humanity’s privacy rights. To do so, we are releasing a series of services, which all have the common goal of helping humanity protect this fundamental right.",
17 | "copyright": {
18 | "line1": "Copyright © ",
19 | "line2": ", Internxt Universal Technologies SL"
20 | },
21 | "comingSoon": "Coming soon",
22 | "new": "New",
23 | "sections": {
24 | "products": {
25 | "title": "Products",
26 | "drive": "Internxt Drive",
27 | "photos": "Internxt Photos",
28 | "send": "Internxt Send",
29 | "webDAV": "WebDAV",
30 | "vpn": "Internxt VPN",
31 | "token": "Token",
32 | "pricing": "Pricing",
33 | "business": "Internxt for Business"
34 | },
35 | "company": {
36 | "title": "Company",
37 | "about": "About Us",
38 | "privacy": "Privacy",
39 | "openSource": "Open Source",
40 | "mediaArea": "Media Area",
41 | "security": "Security",
42 | "legal": "Legal",
43 | "whyInternxt": "Why Internxt?"
44 | },
45 | "join": {
46 | "title": "Join Us",
47 | "newsletter": "Newsletter",
48 | "affiliates": "Affiliates",
49 | "storageForEducation": "Internxt for Education",
50 | "signup": "Create Account",
51 | "support": "Support",
52 | "login": "Log In",
53 | "community": "Community",
54 | "github": "GitHub",
55 | "whitePaper": "White Paper",
56 | "twitter": "Twitter",
57 | "facebook": "Facebook",
58 | "linkedin": "LinkedIn",
59 | "youtube": "Youtube",
60 | "instagram": "Instagram",
61 | "mastodon": "Mastodon"
62 | },
63 | "resources": {
64 | "title": "Resources",
65 | "blog": "Blog",
66 | "comparison": "Cloud Storage Comparison",
67 | "directoryOfPrivacyOrganizations": "Directory of Privacy Organizations",
68 | "pCloudAlternative": "pCloud Alternative",
69 | "cyberAwareness": "Cyber Awareness",
70 | "whatGoogleKnowsAboutMe": "What Google Knows"
71 | },
72 | "tools": {
73 | "title": "Free Tools",
74 | "temporaryEmail": "Temporary Email",
75 | "fileVirusScan": "File Virus Scanner",
76 | "passwordChecker": "Password Checker",
77 | "byteConverter": "Byte Converter",
78 | "passwordGenerator": "Password Generator",
79 | "fileConverter": "File Converter"
80 | }
81 | }
82 | },
83 | "Cookies": {
84 | "title": "Internxt uses cookies to make this website easier to use",
85 | "link": "Learn more about cookies",
86 | "close": "Ok, close"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/assets/lang/en/navbar.json:
--------------------------------------------------------------------------------
1 | {
2 | "links": {
3 | "products": "Products",
4 | "about": "About us",
5 | "pricing": "Pricing",
6 | "ourValues": "Our values",
7 | "login": "Log in",
8 | "getStarted": "Get Internxt",
9 | "checkout": "Claim now!"
10 | },
11 | "products": {
12 | "drive": "Internxt Drive",
13 | "photos": "Internxt Photos",
14 | "send": "Internxt Send",
15 | "s3": "Internxt S3",
16 | "vpn": "Internxt VPN",
17 | "comingSoon": "Coming soon",
18 | "new": "New"
19 | },
20 | "ourValues": {
21 | "privacy": "Privacy",
22 | "openSource": "Open Source"
23 | },
24 | "Auth": {
25 | "SignUp": {
26 | "title": "Create an Internxt account",
27 | "or": "or",
28 | "login": "Log in",
29 | "disclaimer": {
30 | "text": "By creating an account you accept the",
31 | "link": "terms and conditions"
32 | },
33 | "fields": {
34 | "email": {
35 | "label": "Email",
36 | "placeholder": "Email"
37 | },
38 | "password": {
39 | "label": "Password",
40 | "placeholder": "Password",
41 | "strength": {
42 | "complexity": "Password is not complex enough",
43 | "length": "Password has to be at least 8 characters long",
44 | "weak": "Password is weak",
45 | "strong": "Password is strong"
46 | }
47 | },
48 | "submit": "Get started"
49 | }
50 | },
51 | "LogIn": {
52 | "title": "Log in to Internxt",
53 | "or": "or",
54 | "signup": "Create account",
55 | "fields": {
56 | "email": {
57 | "label": "Email",
58 | "placeholder": "Email"
59 | },
60 | "password": {
61 | "label": "Password",
62 | "placeholder": "Password",
63 | "helper": "Forgot your password?"
64 | },
65 | "tfa": {
66 | "label": "Two Factor Authentication",
67 | "placeholder": "2FA Code",
68 | "hint": "2FA Code must be 6 digits"
69 | },
70 | "submit": "Log in"
71 | }
72 | },
73 | "Recover": {
74 | "title": "Recover account",
75 | "back": "Back to log in",
76 | "disclaimer": "As specified in the app settings, only you have access to your password, therefore we can't restore your account without it. You can sign up again by entering your email below so we can delete your account and all your files.",
77 | "fields": {
78 | "email": {
79 | "label": "Email",
80 | "placeholder": "Email"
81 | },
82 | "submit": "Send confirmation email"
83 | },
84 | "success": {
85 | "title": "Check your email",
86 | "subtitle": "We've send an account reset link to"
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/assets/social/black/facebook.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/social/black/instagram.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/social/black/linkedin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/social/black/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-30/facebook.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-30/instagram.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-30/linkedin.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-30/mastodon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-30/twitter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-30/youtube.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-60/facebook.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-60/instagram.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-60/linkedin.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-60/mastodon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-60/twitter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/cool-gray-60/youtube.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/neutral-300/facebook.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/neutral-300/instagram.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/neutral-300/linkedin.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/neutral-300/twitter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/reddit.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/assets/social/white/facebook.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/white/instagram.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/white/linkedin.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/assets/social/white/twitter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | export default function Button({
4 | className = "",
5 | children,
6 | disabled,
7 | outline,
8 | onClick,
9 | }: {
10 | className?: string;
11 | children: ReactNode;
12 | disabled?: boolean;
13 | outline?: boolean;
14 | onClick?: () => void;
15 | }) {
16 | const background = outline
17 | ? "bg-transparent"
18 | : "bg-primary active:bg-primary-dark disabled:bg-gray-40";
19 |
20 | const textColor = outline
21 | ? "text-primary active:text-primary-dark disabled:text-gray-40"
22 | : "text-white";
23 |
24 | const border = outline
25 | ? "border border-primary active:border-primary-dark disabled:border-gray-40"
26 | : "";
27 |
28 | return (
29 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Card.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | export default function Card({
4 | className = "",
5 | children,
6 | }: {
7 | className?: string;
8 | children?: ReactNode;
9 | }) {
10 | return (
11 |
14 | {children}
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/CardBotton.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | export default function CardBottom({ children }: { children: ReactNode }) {
4 | return (
5 |
6 | {children}
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | import { Menu, Transition } from '@headlessui/react';
2 | import { ReactNode, Ref } from 'react';
3 |
4 | export default function Dropdown({
5 | children,
6 | classButton,
7 | menuItems,
8 | classMenuItems,
9 | openDirection,
10 | buttonInputRef
11 | }: {
12 | children: ReactNode;
13 | classButton?: string;
14 | menuItems?: ReactNode[];
15 | classMenuItems: string;
16 | openDirection: 'left' | 'right';
17 | buttonInputRef: Ref;
18 | }): JSX.Element {
19 | return (
20 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/FancySpinner.tsx:
--------------------------------------------------------------------------------
1 | export default function FancySpinner({
2 | className = "",
3 | progress,
4 | }: {
5 | className?: string;
6 | progress: number;
7 | }) {
8 | return (
9 |
10 |
11 |
12 | {progress <= 99 ? progress : 99}
13 |
14 |
%
15 |
16 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/FaqAccordion.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { PlusCircle } from "phosphor-react";
3 | import ReactMarkdown from "react-markdown";
4 |
5 | export default function FaqAccordion({
6 | question,
7 | answer,
8 | isQuestionBigger = false,
9 | }: {
10 | question: string;
11 | answer: string[];
12 | isQuestionBigger?: boolean;
13 | }) {
14 | const [active, setActive] = useState(false);
15 |
16 | return (
17 |
18 |
39 |
40 |
45 | {answer.map((text: any) => {
46 | return {text};
47 | })}
48 |
49 |
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/FileArea.tsx:
--------------------------------------------------------------------------------
1 | import { File, Folder, PlusCircle } from "phosphor-react";
2 | import {
3 | ChangeEvent,
4 | forwardRef,
5 | MouseEvent,
6 | ReactNode,
7 | useContext,
8 | useRef,
9 | useState,
10 | } from "react";
11 | import { FilesContext } from "../contexts/Files";
12 | import { format } from "bytes";
13 | import { MAX_BYTES_PER_SEND, MAX_ITEMS_PER_LINK } from "../constants";
14 | import ItemsList from "./ItemList";
15 | import Dropdown from "./Dropdown";
16 |
17 | export default function FileArea({
18 | className = "",
19 | scroll,
20 | }: {
21 | className?: string;
22 | scroll: boolean;
23 | }) {
24 | const fileInputRef = useRef(null);
25 | const folderInputRef = useRef(null);
26 | const dropdownMenuButtonRef = useRef(null);
27 | const [fileInputKey, setFileInputKey] = useState(Date.now());
28 | const [folderInputKey, setFolderInputKey] = useState(Date.now());
29 |
30 | const fileContext = useContext(FilesContext);
31 |
32 | const onFileExplorerOpen = (e: MouseEvent) => {
33 | e.preventDefault();
34 | fileInputRef.current?.click();
35 | };
36 |
37 | const onFolderExplorerOpen = (e: MouseEvent) => {
38 | e.preventDefault();
39 | folderInputRef.current?.click();
40 | };
41 |
42 | const onInputChange = (event: ChangeEvent) => {
43 | if (event.target.files) {
44 | fileContext.addFiles(Array.from(event.target.files));
45 | }
46 | if (dropdownMenuButtonRef.current?.ariaExpanded === "true") {
47 | dropdownMenuButtonRef.current?.click();
48 | }
49 | setFileInputKey(Date.now());
50 | setFolderInputKey(Date.now());
51 | };
52 |
53 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
54 | const MenuItem = forwardRef(
55 | (
56 | {
57 | children,
58 | onClick,
59 | }: { children: ReactNode; onClick: (e: MouseEvent) => void },
60 | ref
61 | ) => {
62 | return (
63 |
67 | {children}
68 |
69 | );
70 | }
71 | );
72 |
73 | const spaceRemaining = MAX_BYTES_PER_SEND - fileContext.totalFilesSize;
74 |
75 | return (
76 |
77 |
85 |
95 | {fileContext.itemList.length === 0 ? (
96 |
100 | ) : (
101 | <>
102 |
109 |
110 |
121 |
122 | Folders
123 | ,
124 | ,
128 | ]}
129 | >
130 |
131 |
132 |
Add more items
133 |
134 |
135 | {fileContext.totalFilesCount} / {MAX_ITEMS_PER_LINK}{" "}
136 | {fileContext.totalFilesCount > 1 ? "files" : "file"} added
137 |
138 |
·
139 |
{format(spaceRemaining)} remaining
140 |
141 |
142 |
143 | >
144 | )}
145 |
146 | );
147 | }
148 |
149 | function Empty({
150 | onFileExplorerOpen,
151 | onFolderExplorerOpen,
152 | }: {
153 | onFileExplorerOpen: (e: MouseEvent) => void;
154 | onFolderExplorerOpen: (e: MouseEvent) => void;
155 | }) {
156 | return (
157 |
161 |
{
164 | e.stopPropagation();
165 | }}
166 | >
167 |
173 |
174 |
Upload files
175 |
176 |
177 | or select a folder
178 |
179 |
180 |
181 |
182 |
183 | );
184 | }
185 |
186 | declare module "react" {
187 | interface HTMLAttributes extends AriaAttributes, DOMAttributes {
188 | // extends React's HTMLAttributes
189 | directory?: string;
190 | webkitdirectory?: string;
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/components/Input.tsx:
--------------------------------------------------------------------------------
1 | import { WarningOctagon, Warning, CheckCircle } from "phosphor-react";
2 |
3 | export default function Input({
4 | className = "",
5 | label,
6 | type = "text",
7 | accent,
8 | disabled,
9 | placeholder,
10 | value,
11 | onChange,
12 | message,
13 | onFocus,
14 | onBlur,
15 | onPaste,
16 | onKeyDown,
17 | refForInput,
18 | onMouseEnter,
19 | onMouseLeave,
20 | }: {
21 | className?: string;
22 | label?: string;
23 | variant?: "text" | "email";
24 | accent?: "error" | "warning" | "success";
25 | disabled?: boolean;
26 | placeholder?: string;
27 | value?: string;
28 | onChange?: (v: string) => void;
29 | onKeyDown?: React.KeyboardEventHandler;
30 | onPaste?: React.ClipboardEventHandler;
31 | onFocus?: () => void;
32 | onBlur?: () => void;
33 | message?: string;
34 | type?: string;
35 | refForInput?: React.RefObject;
36 | onMouseEnter?: React.MouseEventHandler;
37 | onMouseLeave?: React.MouseEventHandler;
38 | }): JSX.Element {
39 | let focusColor: string;
40 |
41 | switch (accent) {
42 | case "error":
43 | focusColor = "focus:border-red-std ring-red-std";
44 | break;
45 | case "warning":
46 | focusColor = "focus:border-orange ring-orange";
47 | break;
48 | case "success":
49 | focusColor = "focus:border-green ring-green";
50 | break;
51 | default:
52 | focusColor = "focus:border-primary ring-primary";
53 | break;
54 | }
55 |
56 | const borderColor =
57 | "border-gray-20 disabled:border-gray-10 hover:border-gray-30";
58 |
59 | const backgroundColor = "bg-white disabled:bg-white";
60 |
61 | const placeholderColor = "placeholder-gray-30";
62 |
63 | const padding = "px-3";
64 |
65 | const input = (
66 |
67 | onChange && onChange(e.target.value)}
77 | onFocus={() => {
78 | onFocus && onFocus();
79 | }}
80 | onBlur={() => {
81 | onBlur && onBlur();
82 | }}
83 | onPaste={onPaste}
84 | value={value}
85 | onKeyDown={onKeyDown}
86 | />
87 |
88 | );
89 |
90 | let messageColor: string;
91 | let MessageIcon: typeof WarningOctagon | undefined;
92 |
93 | switch (accent) {
94 | case "success":
95 | messageColor = "text-green";
96 | MessageIcon = CheckCircle;
97 | break;
98 | case "warning":
99 | messageColor = "text-orange";
100 | MessageIcon = Warning;
101 | break;
102 | case "error":
103 | messageColor = "text-red-std";
104 | MessageIcon = WarningOctagon;
105 | break;
106 | default:
107 | messageColor = "text-gray-80";
108 | }
109 |
110 | return (
111 |
112 | {label ? (
113 |
120 | ) : (
121 | input
122 | )}
123 | {message && (
124 |
125 | {MessageIcon &&
}
126 |
{message}
127 |
128 | )}
129 |
130 | );
131 | }
132 |
--------------------------------------------------------------------------------
/src/components/ItemList.tsx:
--------------------------------------------------------------------------------
1 | import bytes from "bytes";
2 | import { X } from "phosphor-react";
3 | import { SendItemData } from "../models/SendItem";
4 |
5 | export default function ItemList({
6 | className = "",
7 | items,
8 | onRemoveItem,
9 | }: {
10 | className?: string;
11 | items: SendItemData[];
12 | onRemoveItem?: (item: SendItemData) => void;
13 | }) {
14 | return (
15 |
16 | {items.map((item) => (
17 |
18 | ))}
19 |
20 | );
21 | }
22 |
23 | function Item({
24 | item,
25 | onRemove,
26 | }: {
27 | item: SendItemData;
28 | onRemove?: (item: SendItemData) => void;
29 | }) {
30 | const extension =
31 | item.type === "file" &&
32 | item.name.includes(".") &&
33 | item.name.split(".").pop();
34 |
35 | return (
36 |
37 |
{item.name}
38 |
39 | {item.type === "folder" && (
40 | <>
41 |
Folder
42 |
·
43 | {item.countFiles && (
44 | <>
45 |
46 | {item.countFiles} {item.countFiles === 1 ? "file" : "files"}
47 |
48 |
·
49 | >
50 | )}
51 | >
52 | )}
53 |
{bytes.format(item.size)}
54 | {extension && (
55 | <>
56 |
·
57 |
{extension}
58 | >
59 | )}
60 |
61 | {onRemove && (
62 |
68 | )}
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/NotificationToast.tsx:
--------------------------------------------------------------------------------
1 | import { Transition } from "@headlessui/react";
2 | import { CheckCircle, Info, Warning, WarningOctagon, X } from "phosphor-react";
3 | import { ToastShowProps, ToastType } from "../services/notifications.service";
4 |
5 | const NotificationToast = ({
6 | text,
7 | type,
8 | action,
9 | visible,
10 | closable,
11 | onClose,
12 | }: Omit & {
13 | visible: boolean;
14 | onClose: () => void;
15 | }): JSX.Element => {
16 | let Icon: typeof CheckCircle | undefined;
17 | let IconColor: string | undefined;
18 |
19 | switch (type) {
20 | case ToastType.Success:
21 | Icon = CheckCircle;
22 | IconColor = "text-green";
23 | break;
24 | case ToastType.Error:
25 | Icon = WarningOctagon;
26 | IconColor = "text-red-50";
27 | break;
28 | case ToastType.Info:
29 | Icon = Info;
30 | IconColor = "text-primary";
31 | break;
32 | case ToastType.Warning:
33 | Icon = Warning;
34 | IconColor = "text-yellow";
35 | break;
36 | }
37 |
38 | return (
39 |
49 |
53 | {Icon && (
54 |
55 | )}
56 |
57 |
{text}
58 | {action && (
59 |
65 | )}
66 | {closable && (
67 |
70 | )}
71 |
72 |
73 | );
74 | };
75 |
76 | export default NotificationToast;
77 |
--------------------------------------------------------------------------------
/src/components/RevealX.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 |
3 | interface RevealProps {
4 | children: React.ReactNode;
5 | className?: string;
6 | direction?: "left" | "right";
7 | }
8 |
9 | const RevealX = ({ children, className, direction }: RevealProps) => {
10 | useEffect(() => {
11 | function reveal() {
12 | const reveals = document.querySelectorAll(
13 | direction === "left" ? ".revealXLeft" : ".revealXRight"
14 | );
15 |
16 | for (let i = 0; i < reveals.length; i++) {
17 | const windowHeight = window.innerHeight;
18 | const elementTop = reveals[i].getBoundingClientRect().top;
19 | const elementVisible = 150;
20 |
21 | if (elementTop < windowHeight - elementVisible) {
22 | reveals[i].classList.add("active");
23 | }
24 | }
25 | }
26 |
27 | window.addEventListener("scroll", reveal);
28 |
29 | return () => {
30 | window.removeEventListener("scroll", reveal);
31 | };
32 | }, []);
33 | return (
34 |
39 | {children}
40 |
41 | );
42 | };
43 |
44 | export default RevealX;
45 |
--------------------------------------------------------------------------------
/src/components/RevealY.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 |
3 | interface RevealProps {
4 | children: React.ReactNode;
5 | className?: string;
6 | }
7 |
8 | const RevealY = ({ children, className }: RevealProps) => {
9 | useEffect(() => {
10 | function reveal() {
11 | const reveals = document.querySelectorAll('.revealY');
12 |
13 | for (let i = 0; i < reveals.length; i++) {
14 | const windowHeight = window.innerHeight;
15 | const elementTop = reveals[i].getBoundingClientRect().top;
16 | const elementVisible = 150;
17 |
18 | if (elementTop < windowHeight - elementVisible) {
19 | reveals[i].classList.add('active');
20 | }
21 | // else {
22 | // reveals[i].classList.remove('active');
23 | // }
24 | }
25 | }
26 |
27 | window.addEventListener('scroll', reveal);
28 |
29 | return () => {
30 | window.removeEventListener('scroll', reveal);
31 | };
32 | }, []);
33 | return {children}
;
34 | };
35 |
36 | export default RevealY;
37 |
--------------------------------------------------------------------------------
/src/components/RootDropzone.tsx:
--------------------------------------------------------------------------------
1 | import { format } from "bytes";
2 | import { ReactNode, useCallback, useContext } from "react";
3 | import { useDropzone } from "react-dropzone";
4 | import { MAX_BYTES_PER_SEND } from "../constants";
5 | import { FilesContext } from "../contexts/Files";
6 |
7 | export default function RootDropzone({
8 | className = "",
9 | children,
10 | }: {
11 | className?: string;
12 | children: ReactNode;
13 | }) {
14 | const filesContext = useContext(FilesContext);
15 |
16 | const onDrop = useCallback(
17 | (acceptedFiles: File[]) => {
18 | filesContext.addFiles(acceptedFiles);
19 | },
20 | [filesContext]
21 | );
22 |
23 | const { getRootProps, getInputProps, isDragActive } = useDropzone({
24 | onDrop,
25 | noClick: true,
26 | disabled: !filesContext.enabled,
27 | noKeyboard: true,
28 | });
29 |
30 | const maxBytesPerSendDisplay = format(MAX_BYTES_PER_SEND);
31 |
32 | return (
33 |
34 |
35 | {isDragActive && (
36 |
37 |
38 |
39 |
40 | Drag and drop your files here
41 |
42 |
43 | Maximum {maxBytesPerSendDisplay} upload limit
44 |
45 |
46 |
47 |
48 | )}
49 | {children}
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/SendBanner.tsx:
--------------------------------------------------------------------------------
1 | import { X } from "phosphor-react";
2 | import React from "react";
3 | import { ReactComponent as SendIcon } from "../assets/images/share-file/visual.svg";
4 | import BackgroundImage from "../assets/images/share-file/banner-bg.png";
5 |
6 | export interface Props {
7 | sendBannerVisible: boolean;
8 | setIsSendBannerVisible: (value: boolean) => void;
9 | }
10 |
11 | const SendBanner = (props: Props) => {
12 | const onClose = () => {
13 | props.setIsSendBannerVisible(false);
14 | };
15 |
16 | return (
17 |
22 |
32 |
38 |
39 |
40 |
41 |
42 | Try out Internxt
43 |
44 |
45 | Encrypt, save and share large files with Internxt.
46 |
47 |
48 |
49 |
57 |
58 |
59 |
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default SendBanner;
71 |
--------------------------------------------------------------------------------
/src/components/Spinner.tsx:
--------------------------------------------------------------------------------
1 | const Spinner = ({ className = "" }: { className?: string }): JSX.Element => {
2 | return (
3 |
13 |
14 | );
15 | };
16 |
17 | export default Spinner;
18 |
--------------------------------------------------------------------------------
/src/components/Switch.tsx:
--------------------------------------------------------------------------------
1 | export default function Switch({
2 | className = "",
3 | options,
4 | onClick,
5 | value,
6 | }: {
7 | className?: string;
8 | options: readonly [F, S];
9 | onClick: (value: F | S) => void;
10 | value: F | S;
11 | }) {
12 | const first = options[0];
13 | const second = options[1];
14 |
15 | return (
16 |
19 |
26 |
onClick(first)}
30 | />
31 | onClick(second)}
35 | />
36 |
37 | );
38 | }
39 |
40 | function SwitchPart({
41 | text,
42 | active,
43 | onClick,
44 | }: {
45 | text: string;
46 | active: boolean;
47 | onClick?: () => void;
48 | }): JSX.Element {
49 | return (
50 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/components/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useRef, useState } from "react";
2 |
3 | export default function Tooltip({
4 | children,
5 | title,
6 | subtitle,
7 | popsFrom,
8 | style = "dark",
9 | className,
10 | delayInMs,
11 | show,
12 | position,
13 | }: {
14 | children?: ReactNode;
15 | title: string;
16 | subtitle?: string;
17 | popsFrom: "right" | "left" | "top" | "bottom";
18 | style?: "light" | "dark";
19 | className?: string;
20 | delayInMs?: number;
21 | show?: boolean;
22 | position?: number;
23 | }) {
24 | let tooltipPosition = "";
25 | let trianglePosition = "";
26 | let triangle = "";
27 |
28 | switch (popsFrom) {
29 | case "right":
30 | tooltipPosition = "left-full top-1/2 -translate-y-1/2 ml-1.5";
31 | trianglePosition = "flex-row-reverse";
32 | triangle = "polygon(100% 0%, 100% 100%, 0% 50%)";
33 | break;
34 | case "left":
35 | tooltipPosition = "right-full top-1/2 -translate-y-1/2 mr-1.5";
36 | trianglePosition = "flex-row";
37 | triangle = "polygon(0% 0%, 0% 100%, 100% 50%)";
38 | break;
39 | case "top":
40 | tooltipPosition =
41 | "bottom-full left-1/2 -translate-x-1/2 mb-1.5 origin-bottom";
42 | trianglePosition = "flex-col";
43 | triangle = "polygon(0% 0%, 100% 0%, 50% 100%)";
44 | break;
45 | case "bottom":
46 | tooltipPosition = "top-full left-1/2 -translate-x-1/2 mt-1.5";
47 | trianglePosition = "flex-col-reverse";
48 | triangle = "polygon(50% 0%, 0% 100%, 100% 100%)";
49 | break;
50 | }
51 |
52 | return (
53 |
59 |
64 |
69 |
74 | {title}
75 | {children}
76 |
77 | {subtitle && (
78 |
83 | {subtitle}
84 |
85 | )}
86 |
87 |
98 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/src/components/footer/Footer.module.scss:
--------------------------------------------------------------------------------
1 | @supports not (backdrop-filter: none) {
2 | .cookiesBgFallback {
3 | --tw-bg-opacity: 1;
4 | }
5 | }
--------------------------------------------------------------------------------
/src/components/send/CtaSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import textContent from "../../assets/lang/en/send.json";
3 |
4 | const CtaSection = () => {
5 | return (
6 |
7 |
8 |
9 |
10 | {textContent.CtaSection.title}
11 |
12 |
13 | {textContent.CtaSection.description}
14 |
15 |
16 |
{
18 | window.open("https://internxt.com/pricing", "_blank");
19 | }}
20 | className="flex max-w-[260px] cursor-pointer flex-col items-center rounded-lg bg-white text-center hover:bg-blue-10"
21 | >
22 |
23 | {textContent.CtaSection.cta}
24 |
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default CtaSection;
32 |
--------------------------------------------------------------------------------
/src/components/send/FaqSection.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import FaqAccordion from "../FaqAccordion";
3 |
4 | import label from "../../assets/lang/en/send.json";
5 |
6 | const FaqSection = () => {
7 | return (
8 |
9 |
10 |
11 | {label.FaqSection.title}
12 |
13 |
14 | {label.FaqSection.faq.map((item) => (
15 |
19 |
25 |
26 | ))}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default FaqSection;
34 |
--------------------------------------------------------------------------------
/src/components/send/FeatureSection.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | User,
3 | PaperPlaneTilt,
4 | FileDotted,
5 | Eye,
6 | EyeSlash,
7 | EnvelopeSimple,
8 | } from "phosphor-react";
9 | import label from "../../assets/lang/en/send.json";
10 | import RevealY from "../RevealY";
11 |
12 | const FeatureSection = () => {
13 | const cards = [
14 | {
15 | id: 1,
16 | icon: User,
17 | title: label.FeatureSection.card1.title,
18 | description: label.FeatureSection.card1.description,
19 | },
20 | {
21 | id: 2,
22 | icon: EyeSlash,
23 | title: label.FeatureSection.card2.title,
24 | description: label.FeatureSection.card2.description,
25 | },
26 | {
27 | id: 3,
28 | icon: PaperPlaneTilt,
29 | title: label.FeatureSection.card3.title,
30 | description: label.FeatureSection.card3.description,
31 | },
32 | {
33 | id: 4,
34 | icon: FileDotted,
35 | title: label.FeatureSection.card4.title,
36 | description: label.FeatureSection.card4.description,
37 | },
38 | {
39 | id: 5,
40 | icon: EnvelopeSimple,
41 | title: label.FeatureSection.card5.title,
42 | description: label.FeatureSection.card5.description,
43 | },
44 | {
45 | id: 6,
46 | icon: Eye,
47 | title: label.FeatureSection.card6.title,
48 | description: label.FeatureSection.card6.description,
49 | },
50 | ];
51 |
52 | return (
53 |
54 |
55 |
56 | {label.FeatureSection.title}
57 |
58 |
59 | {label.FeatureSection.description}
60 |
61 |
62 |
63 | {cards.map((card) => (
64 |
68 |
69 |
70 |
{card.title}
71 |
{card.description}
72 |
73 |
74 | ))}
75 |
76 |
77 | );
78 | };
79 |
80 | export default FeatureSection;
81 |
--------------------------------------------------------------------------------
/src/components/send/InfoSection.tsx:
--------------------------------------------------------------------------------
1 | import label from "../../assets/lang/en/send.json";
2 | import RevealX from "../RevealX";
3 |
4 | const urlPrefix = process.env.REACT_APP_BASE_URL || "";
5 |
6 | const InfoSection = () => {
7 | return (
8 |
9 |
10 |
{label.InfoSection.title}
11 |
{label.InfoSection.description}
12 |
13 |
14 |
18 |
19 |
20 | {label.InfoSection.card1.title}
21 |
22 |
23 |
24 | {label.InfoSection.card1.description.line1}
25 |
26 |
27 | {label.InfoSection.card1.description.line2}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |

36 |
37 |
38 |
39 |
40 |
44 |
45 |
46 | {label.InfoSection.card2.title}
47 |
48 |
49 |
50 | {label.InfoSection.card2.description.line1}
51 |
52 |
53 | {label.InfoSection.card2.description.line2}
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |

62 |
63 |
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default InfoSection;
71 |
--------------------------------------------------------------------------------
/src/components/utils/schema-markup-generator.js:
--------------------------------------------------------------------------------
1 | // Schema markup generation funcitons
2 |
3 | // FAQ Data Structure generator
4 | // Additional information: https://developers.google.com/search/docs/appearance/structured-data/faqpage
5 |
6 | export const sm_faq = (faq) => {
7 | let data = `{
8 | "@context": "https://schema.org",
9 | "@type": "FAQPage",
10 | "mainEntity": [`;
11 |
12 | faq.forEach((item, i, arr) => {
13 | data += `{
14 | "@type": "Question",
15 | "name": "${item.question}",
16 | "acceptedAnswer": {
17 | "@type": "Answer",
18 | "text": "`;
19 |
20 | if (item.answer.length > 1) {
21 | item.answer.forEach((answer, i, arr) => {
22 | data += `${answer}
`;
23 | });
24 | } else {
25 | data += `${item.answer[0]}`;
26 | }
27 |
28 | data += `"
29 | }}`;
30 | if (i + 1 < arr.length) data += ",";
31 | });
32 | data += `]}`;
33 | return data;
34 | };
35 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const MAX_BYTES_PER_SEND = 5_368_709_120;
2 | export const MAX_ITEMS_PER_LINK = 100;
3 |
4 | export const MAX_RECIPIENTS = 50;
5 |
--------------------------------------------------------------------------------
/src/contexts/Files.tsx:
--------------------------------------------------------------------------------
1 | import { format } from "bytes";
2 | import { createContext, ReactNode, useEffect, useState } from "react";
3 | import { FileWithPath } from "react-dropzone";
4 | import { MAX_BYTES_PER_SEND, MAX_ITEMS_PER_LINK } from "../constants";
5 | import { SendItemData } from "../models/SendItem";
6 | import { transformInputFilesToJSON, transformJsonFilesToItems } from "../services/items.service";
7 | import notificationsService, {
8 | ToastType,
9 | } from "../services/notifications.service";
10 |
11 | type FilesContextT = {
12 | enabled: boolean;
13 | setEnabled: (value: boolean) => void;
14 | itemList: SendItemData[];
15 | totalFilesCount: number;
16 | totalFilesSize: number;
17 | addFiles: (file: FileWithPath[]) => void;
18 | removeItem: (file: SendItemData) => void;
19 | clear: () => void;
20 | };
21 |
22 | export const FilesContext = createContext({} as FilesContextT);
23 |
24 | export const FilesProvider = ({ children }: { children: ReactNode }) => {
25 | const [itemList, setItemList] = useState([]);
26 | const [totalFilesCount, setTotalFilesCount] = useState(0);
27 | const [totalFilesSize, setTotalFilesSize] = useState(0);
28 | const [enabled, setEnabled] = useState(true);
29 |
30 | useEffect(() => {
31 | setTotalFilesCount(itemList.reduce(
32 | (prev, current) => prev + (current.type === 'folder' ? current.countFiles || 0 : 1),
33 | 0
34 | ));
35 | setTotalFilesSize(itemList.reduce(
36 | (prev, current) => prev + current.size,
37 | 0
38 | ));
39 | }, [itemList]);
40 |
41 | const addFiles = (newFileList: FileWithPath[]) => {
42 | const thereAreEmptyFiles = newFileList.some((file) => file.size === 0);
43 | if (thereAreEmptyFiles) {
44 | notificationsService.show({
45 | text: "Empty files are not supported",
46 | type: ToastType.Warning,
47 | });
48 | return;
49 | }
50 |
51 | const newFilesTotalSize = newFileList.reduce(
52 | (prev, current) => prev + current.size,
53 | 0
54 | );
55 |
56 | if (totalFilesCount + newFileList.length > MAX_ITEMS_PER_LINK) {
57 | return notificationsService.show({
58 | text: `The maximum number of files allowed in total is ${MAX_ITEMS_PER_LINK}`,
59 | type: ToastType.Warning,
60 | });
61 | }
62 |
63 | if (totalFilesSize + newFilesTotalSize <= MAX_BYTES_PER_SEND) {
64 | const filesJson = transformInputFilesToJSON(newFileList);
65 | let { rootFolders, rootFiles } = transformJsonFilesToItems(filesJson);
66 |
67 | rootFolders = rootFolders.filter((folder) => {
68 | const sameNameItems = itemList.filter(item => item.name === folder.name);
69 | return sameNameItems.length === 0;
70 | });
71 |
72 | rootFiles = rootFiles.filter((file) => {
73 | const sameNameItems = itemList.filter(item => item.name === file.name);
74 | return sameNameItems.length === 0;
75 | });
76 |
77 | const sendItemsList = [...rootFolders, ...rootFiles];
78 | setItemList([...itemList, ...sendItemsList]);
79 | } else {
80 | notificationsService.show({
81 | text: `The maximum size allowed is ${format(
82 | MAX_BYTES_PER_SEND
83 | )} in total`,
84 | type: ToastType.Warning,
85 | });
86 | }
87 | }
88 |
89 | const removeItem = (removeItem: SendItemData) => {
90 | setItemList(itemList.filter(item => item.id !== removeItem.id));
91 | };
92 |
93 | const clear = () => {
94 | setItemList([]);
95 | }
96 |
97 | return (
98 |
110 | {children}
111 |
112 | );
113 | };
114 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import * as Sentry from "@sentry/react";
4 | import { BrowserTracing } from "@sentry/tracing";
5 | import "./styles/index.generated.css";
6 | import App from "./App";
7 | import reportWebVitals from "./reportWebVitals";
8 | import notificationsService, {
9 | ToastType,
10 | } from "./services/notifications.service";
11 | import throttle from "lodash.throttle";
12 |
13 | Sentry.init({
14 | dsn: process.env.REACT_APP_SENTRY_DSN,
15 | integrations: [new BrowserTracing()],
16 | tracesSampleRate: 1.0,
17 | debug: process.env.NODE_ENV !== "production",
18 | environment: process.env.NODE_ENV,
19 | });
20 |
21 | function onUnhandledException(e: ErrorEvent | PromiseRejectionEvent) {
22 | console.error(e);
23 |
24 | notificationsService.show({
25 | type: ToastType.Error,
26 | text: "Something went wrong, our team has been notified",
27 | });
28 | }
29 |
30 | const throttledOnUnhandledException = throttle(onUnhandledException, 2000);
31 |
32 | window.addEventListener("error", throttledOnUnhandledException);
33 | window.addEventListener("unhandledrejection", throttledOnUnhandledException);
34 |
35 | const root = ReactDOM.createRoot(
36 | document.getElementById("root") as HTMLElement
37 | );
38 | root.render(
39 |
40 |
41 |
42 | );
43 |
44 | // If you want to start measuring performance in your app, pass a function
45 | // to log results (for example: reportWebVitals(console.log))
46 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
47 | reportWebVitals();
48 |
--------------------------------------------------------------------------------
/src/lib/stringUtils.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest';
2 | import { base64UrlSafetoUUID, fromBase64UrlSafe, generateRandomStringUrlSafe, toBase64UrlSafe } from './stringUtils';
3 |
4 | describe('stringUtils', () => {
5 | describe('toBase64UrlSafe', () => {
6 | it('converts standard Base64 to URL-safe Base64', () => {
7 | const base64 = 'KHh+VVwlYjs/J3E=';
8 | const urlSafe = toBase64UrlSafe(base64);
9 | expect(urlSafe).toBe('KHh-VVwlYjs_J3E');
10 | });
11 |
12 | it('removes trailing "=" characters', () => {
13 | const base64 = 'KHh+VVwlYjs/J3FiYw==';
14 | const urlSafe = toBase64UrlSafe(base64);
15 | expect(urlSafe).toBe('KHh-VVwlYjs_J3FiYw');
16 | });
17 |
18 | it('does not modify already URL-safe Base64 strings', () => {
19 | const base64 = 'KHh-VVwlYjs_J3E';
20 | const urlSafe = toBase64UrlSafe(base64);
21 | expect(urlSafe).toBe(base64);
22 | });
23 | });
24 |
25 | describe('fromBase64UrlSafe', () => {
26 | it('converts URL-safe Base64 back to standard Base64', () => {
27 | const urlSafe = 'KHh-VVwlYjs_J3E';
28 | const base64 = fromBase64UrlSafe(urlSafe);
29 | expect(base64).toBe('KHh+VVwlYjs/J3E=');
30 | });
31 |
32 | it('does not modify already standard Base64 strings', () => {
33 | const urlSafe = 'KHh+VVwlYjs/J3FiYw==';
34 | const base64 = fromBase64UrlSafe(urlSafe);
35 | expect(base64).toBe(urlSafe);
36 | });
37 | });
38 |
39 | describe('generateRandomStringUrlSafe', () => {
40 | const getRandomNumber = (min: number, max: number) => {
41 | min = Math.ceil(min);
42 | max = Math.floor(max);
43 | return Math.floor(Math.random() * (max - min + 1)) + min;
44 | };
45 |
46 | it('generates a string of the specified length', () => {
47 | const length = getRandomNumber(1, 1000);
48 | const randomString = generateRandomStringUrlSafe(length);
49 | expect(randomString).toHaveLength(length);
50 | });
51 |
52 | it('generates a URL-safe string', () => {
53 | const randomString = generateRandomStringUrlSafe(1000);
54 | expect(randomString).not.toMatch(/[+/=]/);
55 | });
56 |
57 | it('throws an error if size is not positive', () => {
58 | expect(() => generateRandomStringUrlSafe(0)).toThrow('Size must be a positive integer');
59 | expect(() => generateRandomStringUrlSafe(-5)).toThrow('Size must be a positive integer');
60 | });
61 | });
62 |
63 | describe('base64UrlSafetoUUID', () => {
64 | it('converts a Base64 URL-safe string to a UUID v4 format', () => {
65 | const base64UrlSafe = '8yqR2seZThOqF4xNngMjyQ';
66 | const uuid = base64UrlSafetoUUID(base64UrlSafe);
67 | expect(uuid).toBe('f32a91da-c799-4e13-aa17-8c4d9e0323c9');
68 | });
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/src/lib/stringUtils.ts:
--------------------------------------------------------------------------------
1 | import { randomBytes } from 'crypto';
2 | import { Buffer } from 'buffer';
3 | import { validate as validateUuidv4 } from 'uuid';
4 |
5 | /**
6 | * Converts a standard Base64 string into a URL-safe Base64 variant:
7 | * - Replaces all "+" characters with "-"
8 | * - Replaces all "/" characters with "_"
9 | * - Strips any trailing "=" padding characters
10 | *
11 | * @param base64 - A standard Base64–encoded string
12 | * @returns The URL-safe Base64 string
13 | */
14 | export const toBase64UrlSafe = (base64: string): string => {
15 | return base64
16 | .replace(/\+/g, '-') // converts "+" to "-"
17 | .replace(/\//g, '_') // converts "/" to "_"
18 | .replace(/=+$/, ''); // removes trailing "="
19 | };
20 |
21 | /**
22 | * Converts a URL-safe Base64 string back into a standard Base64 variant:
23 | * - Replaces all "-" characters with "+"
24 | * - Replaces all "_" characters with "/"
25 | * - Adds "=" padding characters at the end until length is a multiple of 4
26 | *
27 | * @param urlSafe - A URL-safe Base64–encoded string
28 | * @returns The standard Base64 string, including any necessary "=" padding
29 | */
30 | export const fromBase64UrlSafe = (urlSafe: string): string => {
31 | let base64 = urlSafe
32 | .replace(/-/g, '+') // convert "-" back to "+"
33 | .replace(/_/g, '/'); // convert "_" back to "/"
34 |
35 | const missingPadding = (4 - (base64.length % 4)) % 4;
36 | if (missingPadding > 0) {
37 | base64 += '='.repeat(missingPadding);
38 | }
39 | return base64;
40 | };
41 |
42 | /**
43 | * Generates a cryptographically secure, URL-safe string of a given length.
44 | *
45 | * Internally:
46 | * 1. Calculates how many raw bytes are needed to generate at least `size` Base64 chars.
47 | * 2. Generates secure random bytes with `crypto.randomBytes()`.
48 | * 3. Encodes to standard Base64, then makes it URL-safe.
49 | * 4. Truncates the result to `size` characters.
50 | *
51 | * @param size - Desired length of the output string (must be ≥1)
52 | * @returns A URL-safe string exactly `size` characters long
53 | */
54 | export const generateRandomStringUrlSafe = (size: number): string => {
55 | if (size <= 0) {
56 | throw new Error('Size must be a positive integer');
57 | }
58 | // Base64 yields 4 chars per 3 bytes, so it computes the minimum bytes required
59 | const numBytes = Math.ceil((size * 3) / 4);
60 | const buf = randomBytes(numBytes).toString('base64');
61 |
62 | return toBase64UrlSafe(buf).substring(0, size);
63 | };
64 |
65 | /**
66 | * Converts a base64 url safe string to uuid v4
67 | *
68 | * @example in: `8yqR2seZThOqF4xNngMjyQ` out: `f32a91da-c799-4e13-aa17-8c4d9e0323c9`
69 | */
70 | export function base64UrlSafetoUUID(base64UrlSafe: string): string {
71 | const hex = Buffer.from(fromBase64UrlSafe(base64UrlSafe), 'base64').toString('hex');
72 | return `${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20)}`;
73 | }
74 |
75 | /**
76 | * Extracts a send ID from the provided parameter. If the parameter is not a valid UUIDv4,
77 | * it converts the parameter from a Base64 URL-safe string to a UUID.
78 | *
79 | * @param param - The input parameter, which can be either a UUIDv4 or a Base64 URL-safe string.
80 | * @returns The send ID as a UUID string.
81 | */
82 | export const decodeSendId = (param: string) => {
83 | if (!validateUuidv4(param)) {
84 | return base64UrlSafetoUUID(param);
85 | }
86 | return param;
87 | };
88 |
89 | /**
90 | * Encodes a send ID by removing hyphens, converting it from hexadecimal to Base64,
91 | * and then making it URL-safe.
92 | *
93 | * @param sendId - The send ID to be encoded, expected to be a UUID string.
94 | * @returns The encoded send ID as a URL-safe Base64 string.
95 | */
96 | export const encodeSendId = (sendId: string) => {
97 | if (!validateUuidv4(sendId)) {
98 | throw new Error('Send id is not valid');
99 | }
100 | const removedUuidDecoration = sendId.replace(/-/g, '');
101 | const base64endoded = Buffer.from(removedUuidDecoration, 'hex').toString('base64');
102 | const encodedSendId = toBase64UrlSafe(base64endoded);
103 | return encodedSendId;
104 | };
105 |
--------------------------------------------------------------------------------
/src/lib/urls.ts:
--------------------------------------------------------------------------------
1 | const INXT_URL = "https://internxt.com";
2 | const INXT_DRIVE_URL = "https://drive.internxt.com";
3 |
4 | const urls = {
5 | legal: `${INXT_URL}/legal`,
6 | products: {
7 | drive: `${INXT_URL}/drive`,
8 | photos: `${INXT_URL}/photos`,
9 | webdav: `${INXT_URL}/webdav`,
10 | send: "https://send.internxt.com",
11 | pricing: `${INXT_URL}/pricing`,
12 | vpn: `${INXT_URL}/vpn`,
13 | business: `${INXT_URL}/business`,
14 | },
15 | company: {
16 | about: `${INXT_URL}/about`,
17 | privacy: `${INXT_URL}/privacy`,
18 | security: "https://blog.internxt.com/how-internxt-protects-your-data",
19 | openSource: `${INXT_URL}/open-source`,
20 | mediaArea: `${INXT_URL}/media-area`,
21 | legal: `${INXT_URL}/legal`,
22 | },
23 | join: {
24 | newsletter: `${INXT_URL}/newsletter-subscribe`,
25 | signup: `${INXT_DRIVE_URL}/new`,
26 | login: `${INXT_DRIVE_URL}/login`,
27 | support: "https://help.internxt.com",
28 | affiliates: "https://internxt.com/affiliates",
29 | github: "https://github.com/internxt",
30 | whitePaper: `/whitepaper/internxt-white-paper.pdf`,
31 | storageForEducation: `${INXT_URL}/cloud-storage-for-education`,
32 | },
33 | resources: {
34 | blog: "https://blog.internxt.com/",
35 | storageComparison: `${INXT_URL}/cloud-storage-comparison`,
36 | privacyDirectory: `${INXT_URL}/privacy-directory`,
37 | pCloudAlternatives: `${INXT_URL}/pcloud-alternative`,
38 | cyberAwareness: `${INXT_URL}/cyber-awareness`,
39 | whatGoogleKnows: `${INXT_URL}/what-does-google-know-about-me`,
40 | },
41 | tools: {
42 | byteConverter: `${INXT_URL}/byte-converter`,
43 | tempMail: `${INXT_URL}/temporary-email`,
44 | passwordGenerator: `${INXT_URL}/password-generator`,
45 | passwordChecker: `${INXT_URL}/password-checker`,
46 | virusScanner: `${INXT_URL}/virus-scanner`,
47 | fileConverter: `${INXT_URL}/file-converter`,
48 | },
49 | social: {
50 | twitter: "https://twitter.com/Internxt",
51 | reddit: "https://www.reddit.com/r/internxt/",
52 | linkedin: "https://linkedin.com/company/internxt",
53 | instagram: "https://instagram.com/internxt",
54 | youtube:
55 | "https://www.youtube.com/channel/UCW2SxWdVEAEACYuejCgpGwg/featured",
56 | },
57 | };
58 |
59 | export default urls;
60 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/logo_dark.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/models/SendItem.ts:
--------------------------------------------------------------------------------
1 | import { FileWithPath } from "react-dropzone";
2 |
3 | export interface SendItemBasic {
4 | id: string;
5 | name: string;
6 | size: number;
7 | type: 'file' | 'folder';
8 | parent_folder: string | null;
9 | }
10 |
11 | export interface SendItem extends SendItemBasic {
12 | linkId: string;
13 | networkId: string;
14 | encryptionKey: string;
15 | createdAt: Date;
16 | updatedAt: Date;
17 | version: number;
18 | path?: string;
19 | countFiles?: number;
20 | childrenFiles?: SendItem[];
21 | childrenFolders?: SendItem[];
22 | }
23 |
24 | export interface SendItemFile extends SendItemBasic {
25 | file?: FileWithPath;
26 | }
27 |
28 | export interface SendItemFolder extends SendItemBasic {
29 | countFiles?: number;
30 | childrenFiles?: SendItemFile[];
31 | childrenFolders?: SendItemFolder[];
32 | }
33 |
34 | export type SendItemData = SendItemFile & SendItemFolder;
35 |
--------------------------------------------------------------------------------
/src/network/crypto.ts:
--------------------------------------------------------------------------------
1 | import { ShardMeta } from '@internxt/inxt-js/build/lib/models';
2 | import {
3 | Aes256gcmEncrypter,
4 | sha512HmacBuffer,
5 | sha512HmacBufferFromHex,
6 | } from '@internxt/inxt-js/build/lib/utils/crypto';
7 | import { Sha256 } from 'asmcrypto.js';
8 | import { mnemonicToSeed } from 'bip39';
9 | import { Cipher, CipherCCM, createCipheriv, createHash } from 'crypto';
10 | import { streamFileIntoChunks } from './streams';
11 |
12 | const BUCKET_META_MAGIC = [
13 | 66, 150, 71, 16, 50, 114, 88, 160, 163, 35, 154, 65, 162, 213, 226, 215, 70, 138, 57, 61, 52, 19, 210, 170, 38, 164,
14 | 162, 200, 86, 201, 2, 81,
15 | ];
16 |
17 | export function createAES256Cipher(key: Buffer, iv: Buffer): Cipher {
18 | return createCipheriv('aes-256-ctr', key, iv);
19 | }
20 |
21 | export function generateHMAC(
22 | shardMetas: Omit[],
23 | encryptionKey: Buffer,
24 | ): Buffer {
25 | const shardHashesSorted = [...shardMetas].sort((sA, sB) => sA.index - sB.index);
26 | const hmac = sha512HmacBuffer(encryptionKey);
27 |
28 | for (const shardMeta of shardHashesSorted) {
29 | hmac.update(Buffer.from(shardMeta.hash, 'hex'));
30 | }
31 |
32 | return hmac.digest();
33 | }
34 |
35 | function getDeterministicKey(key: string, data: string): Buffer {
36 | const input = key + data;
37 |
38 | return createHash('sha512').update(Buffer.from(input, 'hex')).digest();
39 | }
40 |
41 | async function getBucketKey(mnemonic: string, bucketId: string): Promise {
42 | const seed = (await mnemonicToSeed(mnemonic)).toString('hex');
43 |
44 | return getDeterministicKey(seed, bucketId).toString('hex').slice(0, 64);
45 | }
46 |
47 | export function encryptMeta(fileMeta: string, key: Buffer, iv: Buffer): string {
48 | const cipher: CipherCCM = Aes256gcmEncrypter(key, iv);
49 | const cipherTextBuf = Buffer.concat([cipher.update(fileMeta, 'utf8'), cipher.final()]);
50 | const digest = cipher.getAuthTag();
51 |
52 | return Buffer.concat([digest, iv, cipherTextBuf]).toString('base64');
53 | }
54 |
55 | export async function encryptFilename(mnemonic: string, bucketId: string, filename: string): Promise {
56 | const bucketKey = await getBucketKey(mnemonic, bucketId);
57 | const encryptionKey = sha512HmacBufferFromHex(bucketKey).update(Buffer.from(BUCKET_META_MAGIC)).digest().slice(0, 32);
58 | const encryptionIv = sha512HmacBufferFromHex(bucketKey).update(bucketId).update(filename).digest().slice(0, 32);
59 |
60 | return encryptMeta(filename, encryptionKey, encryptionIv);
61 | }
62 |
63 | /**
64 | * Given a stream and a cipher, encrypt its content
65 | * @param readable Readable stream
66 | * @param cipher Cipher used to encrypt the content
67 | * @returns A readable whose output is the encrypted content of the source stream
68 | */
69 | export function encryptReadable(readable: ReadableStream, cipher: Cipher): ReadableStream {
70 | const reader = readable.getReader();
71 |
72 | const encryptedFileReadable = new ReadableStream({
73 | async start(controller) {
74 | let done = false;
75 |
76 | while (!done) {
77 | const status = await reader.read();
78 |
79 | if (!status.done) {
80 | controller.enqueue(cipher.update(status.value));
81 | }
82 |
83 | done = status.done;
84 | }
85 | controller.close();
86 | },
87 | });
88 |
89 | return encryptedFileReadable;
90 | }
91 |
92 | export async function getEncryptedFile(
93 | plainFile: { stream(): ReadableStream },
94 | cipher: Cipher
95 | ): Promise<[Blob, string]> {
96 | const readable = encryptReadable(plainFile.stream(), cipher).getReader();
97 | const hasher = new Sha256();
98 | const blobParts: ArrayBuffer[] = [];
99 |
100 | let done = false;
101 |
102 | while (!done) {
103 | const status = await readable.read();
104 |
105 | if (!status.done) {
106 | hasher.process(status.value);
107 | blobParts.push(status.value);
108 | }
109 |
110 | done = status.done;
111 | }
112 |
113 | hasher.finish();
114 |
115 | return [
116 | new Blob(blobParts, { type: 'application/octet-stream' }),
117 | createHash('ripemd160').update(Buffer.from(hasher.result as Uint8Array)).digest('hex'),
118 | ];
119 | }
120 |
121 | export function sha256(input: Buffer): Buffer {
122 | return createHash('sha256').update(input).digest();
123 | }
124 |
125 | /**
126 | * Given a stream and a cipher, encrypt its content on pull
127 | * @param readable Readable stream
128 | * @param cipher Cipher used to encrypt the content
129 | * @returns A readable whose output is the encrypted content of the source stream
130 | */
131 | export function encryptReadablePull(readable: ReadableStream, cipher: Cipher): ReadableStream {
132 | const reader = readable.getReader();
133 |
134 | return new ReadableStream({
135 | async pull(controller) {
136 | console.log('2ND_STEP: PULLING');
137 | const status = await reader.read();
138 |
139 | if (!status.done) {
140 | controller.enqueue(cipher.update(status.value));
141 | } else {
142 | controller.close();
143 | }
144 | },
145 | });
146 | }
147 |
148 | export function encryptStreamInParts(
149 | plainFile: { size: number; stream(): ReadableStream },
150 | cipher: Cipher,
151 | parts: number,
152 | ): ReadableStream {
153 | // We include a marginChunkSize because if we split the chunk directly, there will always be one more chunk left, this will cause a mismatch with the urls provided
154 | const marginChunkSize = 1024;
155 | const chunkSize = plainFile.size / parts + marginChunkSize;
156 | const readableFileChunks = streamFileIntoChunks(plainFile.stream(), chunkSize);
157 |
158 | return encryptReadablePull(readableFileChunks, cipher);
159 | }
160 |
161 | export async function processEveryFileBlobReturnHash(
162 | chunkedFileReadable: ReadableStream,
163 | onEveryBlob: (blob: Blob) => Promise,
164 | ): Promise {
165 | const reader = chunkedFileReadable.getReader();
166 | const hasher = new Sha256();
167 |
168 | let done = false;
169 |
170 | while (!done) {
171 | const status = await reader.read();
172 | if (!status.done) {
173 | const value = status.value;
174 | hasher.process(value);
175 | const blob = new Blob([value], { type: 'application/octet-stream' });
176 | await onEveryBlob(blob);
177 | }
178 |
179 | done = status.done;
180 | }
181 |
182 | hasher.finish();
183 |
184 | return createHash('ripemd160')
185 | .update(Buffer.from(hasher.result as Uint8Array))
186 | .digest('hex');
187 | }
188 |
--------------------------------------------------------------------------------
/src/network/download.ts:
--------------------------------------------------------------------------------
1 | import { Network } from '@internxt/sdk/dist/network';
2 | import { Decipher } from 'crypto';
3 | import * as Sentry from '@sentry/react';
4 |
5 | import { sha256 } from './crypto';
6 | import { NetworkFacade } from './NetworkFacade';
7 | import { Abortable } from './requests';
8 | import { joinReadableBinaryStreams } from './streams';
9 |
10 | export type DownloadProgressCallback = (totalBytes: number, downloadedBytes: number) => void;
11 | export type Downloadable = { fileId: string; bucketId: string };
12 |
13 | export function loadWritableStreamPonyfill(): Promise {
14 | const script = document.createElement('script');
15 | script.src = 'https://cdn.jsdelivr.net/npm/web-streams-polyfill@2.0.2/dist/ponyfill.min.js';
16 | document.head.appendChild(script);
17 |
18 | return new Promise((resolve) => {
19 | script.onload = function () {
20 | resolve();
21 | };
22 | });
23 | }
24 |
25 | type BinaryStream = ReadableStream;
26 |
27 | export async function binaryStreamToBlob(stream: BinaryStream): Promise {
28 | const reader = stream.getReader();
29 | const slices: Uint8Array[] = [];
30 |
31 | let finish = false;
32 |
33 | while (!finish) {
34 | const { done, value } = await reader.read();
35 |
36 | if (!done) {
37 | slices.push(value as Uint8Array);
38 | }
39 |
40 | finish = done;
41 | }
42 |
43 | return new Blob(slices);
44 | }
45 |
46 | interface FileInfo {
47 | bucket: string;
48 | mimetype: string;
49 | filename: string;
50 | frame: string;
51 | size: number;
52 | id: string;
53 | created: Date;
54 | hmac: {
55 | value: string;
56 | type: string;
57 | };
58 | erasure?: {
59 | type: string;
60 | };
61 | index: string;
62 | }
63 |
64 | export function getDecryptedStream(
65 | encryptedContentSlices: ReadableStream[],
66 | decipher: Decipher,
67 | ): ReadableStream {
68 | const encryptedStream = joinReadableBinaryStreams(encryptedContentSlices);
69 |
70 | let keepReading = true;
71 |
72 | const decryptedStream = new ReadableStream({
73 | async pull(controller) {
74 | if (!keepReading) return;
75 |
76 | const reader = encryptedStream.getReader();
77 | const status = await reader.read();
78 |
79 | if (status.done) {
80 | controller.close();
81 | } else {
82 | controller.enqueue(decipher.update(status.value));
83 | }
84 |
85 | reader.releaseLock();
86 | },
87 | cancel() {
88 | keepReading = false;
89 | },
90 | });
91 |
92 | return decryptedStream;
93 | }
94 |
95 | async function getFileDownloadStream(
96 | downloadUrls: string[],
97 | decipher: Decipher,
98 | abortController?: AbortController,
99 | ): Promise {
100 | const encryptedContentParts: ReadableStream[] = [];
101 |
102 | for (const downloadUrl of downloadUrls) {
103 | const encryptedStream = await fetch(downloadUrl, { signal: abortController?.signal }).then((res) => {
104 | if (!res.body) {
105 | throw new Error('No content received');
106 | }
107 |
108 | return res.body;
109 | });
110 |
111 | encryptedContentParts.push(encryptedStream);
112 | }
113 |
114 | return getDecryptedStream(encryptedContentParts, decipher);
115 | }
116 |
117 | interface NetworkCredentials {
118 | user: string;
119 | pass: string;
120 | }
121 |
122 | interface IDownloadParams {
123 | bucketId: string;
124 | fileId: string;
125 | creds?: NetworkCredentials;
126 | mnemonic?: string;
127 | encryptionKey?: Buffer;
128 | token?: string;
129 | options?: {
130 | notifyProgress: DownloadProgressCallback;
131 | abortController?: AbortController;
132 | };
133 | }
134 |
135 | type FileStream = ReadableStream;
136 | type DownloadFileResponse = Promise;
137 | type DownloadFileOptions = { notifyProgress: DownloadProgressCallback, abortController?: AbortController };
138 | interface NetworkCredentials {
139 | user: string;
140 | pass: string;
141 | }
142 |
143 | interface DownloadFileParams {
144 | bucketId: string
145 | fileId: string
146 | options?: DownloadFileOptions
147 | }
148 |
149 | interface DownloadOwnFileParams extends DownloadFileParams {
150 | creds: NetworkCredentials
151 | mnemonic: string
152 | token?: never
153 | encryptionKey?: never
154 | }
155 |
156 | interface DownloadSharedFileParams extends DownloadFileParams {
157 | creds?: never
158 | mnemonic?: never
159 | token: string
160 | encryptionKey: string
161 | }
162 |
163 | type DownloadSharedFileFunction = (params: DownloadSharedFileParams) => DownloadFileResponse;
164 | type DownloadOwnFileFunction = (params: DownloadOwnFileParams) => DownloadFileResponse;
165 | type DownloadFileFunction = (params: DownloadSharedFileParams | DownloadOwnFileParams) => DownloadFileResponse;
166 |
167 | const downloadSharedFile: DownloadSharedFileFunction = (params) => {
168 | const { bucketId, fileId, encryptionKey, token, options } = params;
169 |
170 | return new NetworkFacade(
171 | Network.client(
172 | process.env.REACT_APP_NETWORK_URL as string,
173 | {
174 | clientName: 'drive-web',
175 | clientVersion: '1.0'
176 | },
177 | {
178 | bridgeUser: '',
179 | userId: ''
180 | }
181 | )
182 | ).download(bucketId, fileId, '', {
183 | key: Buffer.from(encryptionKey, 'hex'),
184 | token,
185 | downloadingCallback: options?.notifyProgress,
186 | abortController: options?.abortController
187 | });
188 | };
189 |
190 | function getAuthFromCredentials(creds: NetworkCredentials): { username: string, password: string } {
191 | return {
192 | username: creds.user,
193 | password: sha256(Buffer.from(creds.pass)).toString('hex'),
194 | };
195 | }
196 |
197 | const downloadOwnFile: DownloadOwnFileFunction = (params) => {
198 | const { bucketId, fileId, mnemonic, options } = params;
199 | const auth = getAuthFromCredentials(params.creds);
200 |
201 | return new NetworkFacade(
202 | Network.client(
203 | process.env.REACT_APP_NETWORK_URL as string,
204 | {
205 | clientName: 'drive-web',
206 | clientVersion: '1.0'
207 | },
208 | {
209 | bridgeUser: auth.username,
210 | userId: auth.password
211 | }
212 | )
213 | ).download(bucketId, fileId, mnemonic, {
214 | downloadingCallback: options?.notifyProgress,
215 | abortController: options?.abortController
216 | });
217 | };
218 |
219 | const downloadFile: DownloadFileFunction = (params) => {
220 | if (params.token && params.encryptionKey) {
221 | return downloadSharedFile(params);
222 | } else if (params.creds && params.mnemonic) {
223 | return downloadOwnFile(params);
224 | } else {
225 | throw new Error('DOWNLOAD ERRNO. 0');
226 | }
227 | };
228 |
229 | export default downloadFile;
230 |
--------------------------------------------------------------------------------
/src/network/requests.ts:
--------------------------------------------------------------------------------
1 | export interface Abortable {
2 | abort: () => void;
3 | }
4 |
--------------------------------------------------------------------------------
/src/network/streams.ts:
--------------------------------------------------------------------------------
1 | type BinaryStream = ReadableStream;
2 |
3 | export async function binaryStreamToBlob(stream: BinaryStream): Promise {
4 | const reader = stream.getReader();
5 | const slices: Uint8Array[] = [];
6 |
7 | let finish = false;
8 |
9 | while (!finish) {
10 | const { done, value } = await reader.read();
11 |
12 | if (!done) {
13 | slices.push(value as Uint8Array);
14 | }
15 |
16 | finish = done;
17 | }
18 |
19 | return new Blob(slices);
20 | }
21 |
22 | export function buildProgressStream(source: BinaryStream, onRead: (readBytes: number) => void): BinaryStream {
23 | const reader = source.getReader();
24 | let readBytes = 0;
25 |
26 | return new ReadableStream({
27 | async pull(controller) {
28 | const status = await reader.read();
29 |
30 | if (status.done) {
31 | controller.close();
32 | } else {
33 | readBytes += (status.value as Uint8Array).length;
34 |
35 | onRead(readBytes);
36 | controller.enqueue(status.value);
37 | }
38 | },
39 | cancel() {
40 | return reader.cancel();
41 | }
42 | });
43 | }
44 |
45 | export function joinReadableBinaryStreams(streams: BinaryStream[]): ReadableStream {
46 | const streamsCopy = streams.map(s => s);
47 | let keepReading = true;
48 |
49 | const flush = () => streamsCopy.forEach(s => s.cancel());
50 |
51 | const stream = new ReadableStream({
52 | async pull(controller) {
53 | if (!keepReading) return flush();
54 |
55 | const downStream = streamsCopy.shift();
56 |
57 | if (!downStream) {
58 | return controller.close();
59 | }
60 |
61 | const reader = downStream.getReader();
62 | let done = false;
63 |
64 | while (!done && keepReading) {
65 | const status = await reader.read();
66 |
67 | if (!status.done) {
68 | controller.enqueue(status.value);
69 | }
70 |
71 | done = status.done;
72 | }
73 |
74 | reader.releaseLock();
75 | },
76 | cancel() {
77 | keepReading = false;
78 | }
79 | });
80 |
81 | return stream;
82 | }
83 |
84 | /**
85 | * Given a stream of a file, it will read it and enqueue its parts in chunkSizes
86 | * @param readable Readable stream
87 | * @param chunkSize The chunkSize in bytes that we want each chunk to be
88 | * @returns A readable whose output is chunks of the file of size chunkSize
89 | */
90 | export function streamFileIntoChunks(
91 | readable: ReadableStream,
92 | chunkSize: number,
93 | ): ReadableStream {
94 | const reader = readable.getReader();
95 | let buffer = new Uint8Array(0);
96 |
97 | return new ReadableStream({
98 | async pull(controller) {
99 | function handleDone() {
100 | if (buffer.byteLength > 0) {
101 | controller.enqueue(buffer);
102 | }
103 | return controller.close();
104 | }
105 |
106 | const status = await reader.read();
107 |
108 | if (status.done) return handleDone();
109 |
110 | const chunk = status.value;
111 | buffer = mergeBuffers(buffer, chunk);
112 |
113 | while (buffer.byteLength < chunkSize) {
114 | const status = await reader.read();
115 |
116 | if (status.done) return handleDone();
117 |
118 | buffer = mergeBuffers(buffer, status.value);
119 | }
120 |
121 | controller.enqueue(buffer.slice(0, chunkSize));
122 | buffer = new Uint8Array(buffer.slice(chunkSize));
123 | },
124 | });
125 | }
126 |
127 | function mergeBuffers(buffer1: Uint8Array, buffer2: Uint8Array): Uint8Array {
128 | const mergedBuffer = new Uint8Array(buffer1.length + buffer2.length);
129 | mergedBuffer.set(buffer1);
130 | mergedBuffer.set(buffer2, buffer1.length);
131 | return mergedBuffer;
132 | }
133 |
--------------------------------------------------------------------------------
/src/network/upload.ts:
--------------------------------------------------------------------------------
1 | import * as Sentry from "@sentry/react";
2 | import { Network } from "@internxt/sdk/dist/network";
3 | import { ErrorWithContext } from "@internxt/sdk/dist/network/errors";
4 |
5 | import { sha256 } from "./crypto";
6 | import { NetworkFacade } from "./NetworkFacade";
7 | import { reportError } from "../services/error-reporting.service";
8 |
9 | export type UploadProgressCallback = (
10 | totalBytes: number,
11 | uploadedBytes: number
12 | ) => void;
13 |
14 | interface NetworkCredentials {
15 | user: string;
16 | pass: string;
17 | }
18 |
19 | interface IUploadParams {
20 | filesize: number;
21 | filecontent: File;
22 | creds: NetworkCredentials;
23 | mnemonic: string;
24 | progressCallback: UploadProgressCallback;
25 | abortController?: AbortController;
26 | parts?: number;
27 | }
28 |
29 | export function uploadFileBlob(
30 | encryptedFile: Blob,
31 | url: string,
32 | opts: {
33 | progressCallback: UploadProgressCallback;
34 | abortController?: AbortController;
35 | }
36 | ): Promise {
37 | const uploadRequest = new XMLHttpRequest();
38 |
39 | if (opts.abortController?.signal && opts.abortController.signal.aborted) {
40 | throw new Error("Upload aborted");
41 | }
42 |
43 | opts.abortController?.signal.addEventListener(
44 | "abort",
45 | () => {
46 | uploadRequest.abort();
47 | },
48 | { once: true }
49 | );
50 |
51 | uploadRequest.upload.addEventListener("progress", (e) => {
52 | opts.progressCallback(e.total, e.loaded);
53 | });
54 | uploadRequest.upload.addEventListener("loadstart", (e) =>
55 | opts.progressCallback(e.total, 0)
56 | );
57 | uploadRequest.upload.addEventListener("loadend", (e) =>
58 | opts.progressCallback(e.total, e.total)
59 | );
60 |
61 | const uploadFinishedPromise = new Promise(
62 | (resolve, reject) => {
63 | uploadRequest.onload = () => {
64 | if (uploadRequest.status !== 200) {
65 | return reject(
66 | new Error(
67 | "Upload failed with code " +
68 | uploadRequest.status +
69 | " message " +
70 | uploadRequest.response
71 | )
72 | );
73 | }
74 | resolve(uploadRequest);
75 | };
76 | uploadRequest.onerror = reject;
77 | uploadRequest.onabort = () => reject(new Error("Upload aborted"));
78 | uploadRequest.ontimeout = () => reject(new Error("Request timeout"));
79 | }
80 | );
81 |
82 | uploadRequest.open("PUT", url);
83 | // ! Uncomment this line for multipart to work:
84 | // uploadRequest.setRequestHeader('Content-Type', '');
85 | uploadRequest.send(encryptedFile);
86 |
87 | return uploadFinishedPromise;
88 | }
89 |
90 | function getAuthFromCredentials(creds: NetworkCredentials): {
91 | username: string;
92 | password: string;
93 | } {
94 | return {
95 | username: creds.user,
96 | password: sha256(Buffer.from(creds.pass)).toString("hex"),
97 | };
98 | }
99 |
100 | export function uploadFile(
101 | bucketId: string,
102 | params: IUploadParams
103 | ): Promise {
104 | const file: File = params.filecontent;
105 |
106 | const auth = getAuthFromCredentials({
107 | user: params.creds.user,
108 | pass: params.creds.pass,
109 | });
110 |
111 | const facade = new NetworkFacade(
112 | Network.client(
113 | process.env.REACT_APP_NETWORK_URL as string,
114 | {
115 | clientName: "drive-web",
116 | clientVersion: "1.0",
117 | },
118 | {
119 | bridgeUser: auth.username,
120 | userId: auth.password,
121 | }
122 | )
123 | );
124 |
125 | if (params.parts) {
126 | return facade
127 | .uploadMultipart(bucketId, params.mnemonic, file, {
128 | uploadingCallback: params.progressCallback,
129 | abortController: params.abortController,
130 | parts: params.parts,
131 | })
132 | .catch((err: ErrorWithContext) => {
133 | reportError(err, err.context);
134 | throw err;
135 | });
136 | }
137 |
138 | return facade
139 | .upload(bucketId, params.mnemonic, file, {
140 | uploadingCallback: params.progressCallback,
141 | abortController: params.abortController,
142 | })
143 | .catch((err: ErrorWithContext) => {
144 | reportError(err, err.context);
145 |
146 | throw err;
147 | });
148 | }
149 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare namespace NodeJS {
4 | interface ProcessEnv {
5 | NODE_ENV: "development" | "production" | "test";
6 | PUBLIC_URL: string;
7 | REACT_APP_SEND_API_URL: string;
8 | REACT_APP_CRYPTO_SECRET: string;
9 | REACT_APP_CRYPTO_SECRET2: string;
10 | REACT_APP_NETWORK_URL: string;
11 | REACT_APP_PROXY: string;
12 |
13 | REACT_APP_SEND_USER: string;
14 | REACT_APP_SEND_PASS: string;
15 | REACT_APP_SEND_ENCRYPTION_KEY: string;
16 | REACT_APP_SEND_BUCKET_ID: string;
17 | REACT_APP_BASE_URL: string;
18 | }
19 | }
20 |
21 | interface Window {
22 | grecaptcha: {
23 | ready: (cb: () => void) => void;
24 | execute: (siteKey: string, { action: string }) => Promise;
25 | };
26 | gtag: any;
27 | rudderanalytics: any;
28 | }
29 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/services/analytics.service.ts:
--------------------------------------------------------------------------------
1 | const CONTEXT_APP_NAME = "send-web";
2 |
3 | const track = (eventName: string, dataToSend: Record) => {
4 | window.rudderanalytics.track(eventName, dataToSend, {
5 | context: {
6 | app: {
7 | name: CONTEXT_APP_NAME,
8 | },
9 | },
10 | });
11 | };
12 |
13 | const analyticsService = {
14 | track,
15 | };
16 |
17 | export default analyticsService;
18 |
--------------------------------------------------------------------------------
/src/services/download.service.ts:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import fileDownload from "js-file-download";
3 |
4 | import { NetworkService, ProgressOptions } from "./network.service";
5 | import { FlatFolderZip } from "./zip/FlatFolderZip";
6 |
7 | import { binaryStreamToBlob } from "../network/streams";
8 | import { SendItem } from "../models/SendItem";
9 | import { StreamService } from "./stream.service";
10 | import { getAllItemsList } from "./items.service";
11 |
12 |
13 | /**
14 | * This service has *the only responsability* of downloading
15 | * the content via browser to the user filesystem.
16 | */
17 | export class DownloadService {
18 | static async downloadFilesFromLink(
19 | linkId: string,
20 | opts?: ProgressOptions
21 | ): Promise {
22 | const { title, items, code } = await getSendLink(linkId);
23 | const date = new Date();
24 | const now = date.getFullYear() + String(date.getMonth() + 1).padStart(2, '0')
25 | + String(date.getDate()).padStart(2, '0') + String(date.getHours()).padStart(2, '0')
26 | + String(date.getMinutes()).padStart(2, '0') + String(date.getSeconds()).padStart(2, '0')
27 | + String(date.getMilliseconds()).padStart(3, '0');
28 |
29 | await DownloadService.downloadFiles(
30 | title && String(title).trim().length > 0 ? title : 'internxt-send_' + now,
31 | items,
32 | NetworkService.getInstance(),
33 | opts?.plainCode || code,
34 | opts
35 | );
36 | }
37 |
38 | static async downloadFiles(
39 | zipName: string,
40 | items: SendItem[],
41 | networkService: NetworkService,
42 | plainCode: string,
43 | opts?: ProgressOptions
44 | ) {
45 | const totalBytes = items.reduce((a, f) => a + (f.type === 'file' ? f.size : 0), 0);
46 |
47 | const itemList = getAllItemsList(items);
48 |
49 | const options = {
50 | progress: (downloadedBytes: number) => {
51 | opts?.progress?.(opts.totalBytes || totalBytes, Math.min(downloadedBytes, opts.totalBytes || totalBytes));
52 | },
53 | abortController: opts?.abortController
54 | };
55 |
56 | if (itemList.length > 1 || (itemList.length === 1 && itemList[0].type === 'folder')) {
57 | const zip = new FlatFolderZip(zipName, options);
58 |
59 | for (const item of itemList) {
60 | await this.addItemToZip(zip, item, networkService, plainCode, opts);
61 | }
62 |
63 | await zip.close();
64 | } else {
65 | const [firstItem] = itemList;
66 | if (firstItem.type === 'file') {
67 | const itemDownloadStream = await networkService.getDownloadFileStream(
68 | firstItem,
69 | plainCode,
70 | opts
71 | );
72 |
73 | const oneGigabyte = 1 * 1024 * 1024 * 1024;
74 | if (firstItem.size > oneGigabyte) {
75 | await StreamService.pipeReadableToFileSystemStream(
76 | itemDownloadStream,
77 | firstItem.name,
78 | options
79 | )
80 | } else {
81 | fileDownload(
82 | await binaryStreamToBlob(itemDownloadStream),
83 | firstItem.name
84 | );
85 | }
86 | } else if (firstItem.type === 'folder') {
87 | const zip = new FlatFolderZip(firstItem.name, options);
88 | await this.addItemToZip(zip, firstItem, networkService, plainCode, opts);
89 | await zip.close();
90 | }
91 | }
92 | }
93 |
94 | static async addItemToZip(zip: FlatFolderZip, item: SendItem,
95 | networkService: NetworkService,
96 | plainCode: string,
97 | opts?: ProgressOptions) {
98 | if (item.type === 'file') {
99 | const itemDownloadStream = await networkService.getDownloadFileStream(
100 | item,
101 | plainCode
102 | );
103 | zip.addFile(item.path || item.name, itemDownloadStream);
104 | } else if (item.type === 'folder') {
105 | zip.addFolder(item.path || item.name);
106 | }
107 | if (item.childrenFiles && item.childrenFiles.length > 0) {
108 | await Promise.all(item.childrenFiles.map((childrenFile) => {
109 | return this.addItemToZip(zip, childrenFile, networkService, plainCode, opts);
110 | }));
111 | }
112 |
113 | if (item.childrenFolders && item.childrenFolders.length > 0) {
114 | await Promise.all(item.childrenFolders.map((childrenFolder) => {
115 | return this.addItemToZip(zip, childrenFolder, networkService, plainCode, opts);
116 | }));
117 | }
118 | }
119 | }
120 |
121 | /**
122 | * TODO: SDK
123 | */
124 | export interface GetSendLinkResponse {
125 | id: string;
126 | title: string;
127 | subject: string;
128 | code: string;
129 | views: number;
130 | userId: number | null;
131 | items: SendItem[];
132 | createdAt: string;
133 | updatedAt: string;
134 | expirationAt: string;
135 | size: number;
136 | }
137 |
138 | export async function getSendLink(
139 | linkId: string
140 | ): Promise {
141 | const res = await axios.get(
142 | process.env.REACT_APP_SEND_API_URL + "/links/" + linkId
143 | );
144 |
145 | return res.data;
146 | }
147 |
--------------------------------------------------------------------------------
/src/services/error-reporting.service.ts:
--------------------------------------------------------------------------------
1 | import * as Sentry from "@sentry/react";
2 |
3 | export function reportError(exception: any, extra: Record) {
4 | Sentry.captureException(exception, { extra });
5 | }
6 |
--------------------------------------------------------------------------------
/src/services/network.service.ts:
--------------------------------------------------------------------------------
1 | import downloadFile from "../network/download";
2 | import { uploadFile } from "../network/upload";
3 | import axios, { AxiosRequestConfig } from 'axios';
4 | import { createHash } from 'crypto';
5 | import { aes } from "@internxt/lib";
6 | import { SendItem } from "../models/SendItem";
7 |
8 | interface NetworkCredentials {
9 | user: string;
10 | pass: string;
11 | }
12 |
13 | function getSendAccountParameters(): {
14 | bucketId: string,
15 | user: string,
16 | pass: string,
17 | encryptionKey: string
18 | } {
19 | return {
20 | bucketId: process.env.REACT_APP_SEND_BUCKET_ID,
21 | user: process.env.REACT_APP_SEND_USER,
22 | pass: process.env.REACT_APP_SEND_PASS,
23 | encryptionKey: process.env.REACT_APP_SEND_ENCRYPTION_KEY
24 | }
25 | }
26 |
27 | export type ProgressOptions = {
28 | totalBytes?: number;
29 | progress?: (totalBytes: number, downloadedBytes: number) => void;
30 | abortController?: AbortController;
31 | plainCode?: string;
32 | };
33 |
34 | /**
35 | * This service has *the only responsability* of providing access
36 | * to the content of Send ites, knowing how Send is using the Internxt's
37 | * Network and how the zero-knowledge is being implemented.
38 | */
39 | export class NetworkService {
40 | private constructor(private readonly creds: NetworkCredentials) { }
41 | public static getInstance(): NetworkService {
42 | const accountParams = getSendAccountParameters();
43 |
44 | return new NetworkService({
45 | user: accountParams.user,
46 | pass: accountParams.pass
47 | });
48 | }
49 |
50 | /**
51 | * Downloads a Send item using the decrypting protocol version 1.
52 | */
53 | getDownloadFileStreamV1(
54 | item: SendItem,
55 | encryptedCode: string,
56 | opts?: ProgressOptions
57 | ): Promise {
58 | const { bucketId, encryptionKey } = getSendAccountParameters();
59 | const plainCode = aes.decrypt(encryptedCode, encryptionKey);
60 |
61 | return downloadFile({
62 | bucketId,
63 | fileId: item.networkId,
64 | creds: this.creds,
65 | mnemonic: aes.decrypt(item.encryptionKey, plainCode),
66 | options: {
67 | notifyProgress: (totalBytes, downloadedBytes) => {
68 | opts?.progress?.(opts.totalBytes || totalBytes, downloadedBytes);
69 | },
70 | abortController: opts?.abortController
71 | }
72 | });
73 | }
74 |
75 | /**
76 | * Downloads a Send item using the decrypting protocol version 2.
77 | */
78 | getDownloadFileStreamV2(
79 | item: SendItem,
80 | plainCode: string,
81 | opts?: ProgressOptions
82 | ): Promise {
83 | const { bucketId } = getSendAccountParameters();
84 | const encryptionKey = aes.decrypt(item.encryptionKey, plainCode);
85 |
86 | return downloadFile({
87 | bucketId,
88 | fileId: item.networkId,
89 | creds: this.creds,
90 | mnemonic: encryptionKey,
91 | options: {
92 | notifyProgress: (totalBytes, downloadedBytes) => {
93 | opts?.progress?.(opts.totalBytes || totalBytes, downloadedBytes);
94 | },
95 | abortController: opts?.abortController
96 | }
97 | });
98 | }
99 |
100 | public getDownloadFileStream(
101 | item: SendItem,
102 | code: string,
103 | opts?: ProgressOptions & { customEncryptionKey?: string }
104 | ): Promise {
105 | const requiresVersionTwoDecryption = item.version === 2;
106 |
107 | if (requiresVersionTwoDecryption) {
108 | return this.getDownloadFileStreamV2(
109 | item,
110 | code,
111 | opts
112 | );
113 | } else {
114 | return this.getDownloadFileStreamV1(
115 | item,
116 | code,
117 | opts
118 | );
119 | }
120 | }
121 |
122 | get encryptionKey(): string {
123 | return process.env.REACT_APP_SEND_ENCRYPTION_KEY;
124 | }
125 |
126 | async uploadFile(
127 | file: File,
128 | opts?: {
129 | progress?: (totalBytes: number, downloadedBytes: number) => void,
130 | abortController?: AbortController
131 | }
132 | ): Promise {
133 | const { bucketId, user, pass, encryptionKey } = getSendAccountParameters();
134 |
135 | let parts
136 | const partSize = 50 * 1024 * 1024;
137 | const minimumMultipartThreshold = 100 * 1024 * 1024;
138 |
139 | if (file.size > minimumMultipartThreshold) {
140 | parts = Math.ceil(file.size / partSize);
141 | }
142 |
143 | return uploadFile(bucketId, {
144 | creds: { user, pass },
145 | filecontent: file,
146 | filesize: file.size,
147 | mnemonic: encryptionKey,
148 | progressCallback: (totalBytes, uploadedBytes) => {
149 | opts?.progress?.(totalBytes, uploadedBytes);
150 | },
151 | parts,
152 | abortController: opts?.abortController
153 | });
154 | }
155 |
156 | async getShareToken(): Promise {
157 | // TODO: Move to SDK
158 | const bucketId = process.env.REACT_APP_SEND_BUCKET_ID;
159 | const operation = 'PULL';
160 | const requestUrl = `${process.env.REACT_APP_NETWORK_URL}/buckets/${bucketId}/tokens`;
161 |
162 | function sha256(input: Buffer): Buffer {
163 | return createHash('sha256').update(input).digest();
164 | }
165 |
166 | const opts: AxiosRequestConfig = {
167 | method: 'POST',
168 | auth: {
169 | username: process.env.REACT_APP_SEND_USER,
170 | password: sha256(Buffer.from(process.env.REACT_APP_SEND_PASS)).toString('hex'),
171 | },
172 | url: requestUrl,
173 | data: { operation }
174 | };
175 |
176 | const res = await axios.request<{
177 | bucket: string
178 | encryptionKey: string,
179 | id: string,
180 | operation: 'PULL',
181 | token: string;
182 | }>(opts);
183 |
184 | return res.data.token;
185 | }
186 |
187 | async uploadFileForShare(
188 | file: File,
189 | opts?: {
190 | progress?: (totalBytes: number, downloadedBytes: number) => void,
191 | abortController?: AbortController
192 | }
193 | ): Promise {
194 | const { bucketId, user, pass, encryptionKey } = getSendAccountParameters();
195 |
196 | let parts
197 | const partSize = 100 * 1024 * 1024;
198 | const minimumMultipartThreshold = 100 * 1024 * 1024;
199 |
200 | if (file.size > minimumMultipartThreshold) {
201 | parts = Math.ceil(file.size / partSize);
202 | }
203 |
204 | return uploadFile(bucketId, {
205 | creds: { user, pass },
206 | filecontent: file,
207 | filesize: file.size,
208 | mnemonic: encryptionKey,
209 | progressCallback: (totalBytes, uploadedBytes) => {
210 | opts?.progress?.(totalBytes, uploadedBytes);
211 | },
212 | parts,
213 | abortController: opts?.abortController
214 | });
215 | }
216 | }
217 |
--------------------------------------------------------------------------------
/src/services/notifications.service.ts:
--------------------------------------------------------------------------------
1 | import { createElement } from "react";
2 | import toast from "react-hot-toast";
3 | import NotificationToast from "../components/NotificationToast";
4 |
5 | export enum ToastType {
6 | Success = "success",
7 | Error = "error",
8 | Warning = "warning",
9 | Info = "info",
10 | }
11 |
12 | export type ToastShowProps = {
13 | text: string;
14 | type?: ToastType;
15 | action?: { text: string; onClick: () => void };
16 | duration?: number;
17 | closable?: boolean;
18 | };
19 |
20 | const notificationsService = {
21 | show: ({
22 | text,
23 | type,
24 | action,
25 | duration = 5000,
26 | closable = true,
27 | }: ToastShowProps): string => {
28 | const id = toast.custom(
29 | (t) =>
30 | createElement(NotificationToast, {
31 | text,
32 | type,
33 | visible: t.visible,
34 | action,
35 | closable,
36 | onClose() {
37 | toast.dismiss(id);
38 | },
39 | }),
40 | { duration }
41 | );
42 | return id;
43 | },
44 | dismiss: toast.dismiss,
45 | };
46 |
47 | export default notificationsService;
48 |
--------------------------------------------------------------------------------
/src/services/stream.service.ts:
--------------------------------------------------------------------------------
1 | import fileDownload from 'js-file-download';
2 | import streamSaver from 'streamsaver';
3 |
4 | import { loadWritableStreamPonyfill } from "../network/download";
5 | import { binaryStreamToBlob, buildProgressStream } from '../network/streams';
6 |
7 | function isBrave() {
8 | const maybeBrave = (window.navigator as { brave?: any }).brave;
9 |
10 | return maybeBrave !== undefined && maybeBrave.isBrave.name === "isBrave";
11 | }
12 |
13 | export class StreamService {
14 | static async pipeReadableToFileSystemStream(
15 | readable: ReadableStream,
16 | fileSystemName: string,
17 | opts: {
18 | progress: (readBytes: number) => void,
19 | abortController?: AbortController
20 | }
21 | ): Promise {
22 | const { progress } = opts;
23 | const passThrough = progress ? buildProgressStream(readable, progress) : readable;
24 |
25 | if (isBrave()) {
26 | fileDownload(await binaryStreamToBlob(passThrough), fileSystemName);
27 | return;
28 | }
29 |
30 | const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
31 | if (isFirefox) {
32 | await loadWritableStreamPonyfill();
33 |
34 | streamSaver.WritableStream = window.WritableStream;
35 | }
36 |
37 | await passThrough.pipeTo(
38 | streamSaver.createWriteStream(fileSystemName),
39 | { signal: opts.abortController?.signal }
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/services/upload.service.test.ts:
--------------------------------------------------------------------------------
1 | import { vi, describe, it, expect, beforeEach, Mock } from "vitest";
2 | import { getCaptchaToken } from "../lib/auth";
3 | import {
4 | CreateSendLinksResponse,
5 | SendLink,
6 | UploadService,
7 | } from "./upload.service";
8 | import { FileWithPath } from "react-dropzone";
9 | import { SendItemData } from "../models/SendItem";
10 | import axios from "axios";
11 |
12 | vi.mock("axios");
13 |
14 | vi.mock("../lib/auth", () => ({
15 | getCaptchaToken: vi.fn().mockResolvedValue("mock-token"),
16 | }));
17 |
18 | vi.mock("./upload.service", async () => {
19 | const mod = await vi.importActual(
20 | "./upload.service"
21 | );
22 | return {
23 | ...mod,
24 | };
25 | });
26 |
27 | const mockSendLink: SendLink = {
28 | id: "mock-id-12345",
29 | name: "example-file.txt",
30 | type: "file",
31 | size: 1024,
32 | networkId: "network-id-67890",
33 | encryptionKey: "encryption-key-abcdef",
34 | parent_folder: null,
35 | };
36 |
37 | const mockSendLinkFolder: SendLink = {
38 | id: "mock-id-54321",
39 | name: "example-folder",
40 | type: "folder",
41 | size: 0,
42 | networkId: "network-id-09876",
43 | encryptionKey: "encryption-key-zyxwvu",
44 | parent_folder: "parent-folder-id",
45 | };
46 |
47 | const mockCreateSendLinksResponse: CreateSendLinksResponse = {
48 | id: "mock-id-12345",
49 | title: "Example Send Link",
50 | subject: "Example Subject",
51 | code: "mock-code-abcdef",
52 | sender: "mock-sender@example.com",
53 | receivers: ["receiver1@example.com", "receiver2@example.com"],
54 | views: 5,
55 | userId: 42,
56 | items: [
57 | {
58 | id: "item-id-1",
59 | name: "example-file.txt",
60 | type: "file",
61 | size: 1024, // 1 KB
62 | networkId: "network-id-67890",
63 | encryptionKey: "encryption-key-abcdef",
64 | parent_folder: null,
65 | },
66 | {
67 | id: "item-id-2",
68 | name: "example-folder",
69 | type: "folder",
70 | size: 0,
71 | networkId: "network-id-09876",
72 | encryptionKey: "encryption-key-zyxwvu",
73 | parent_folder: "parent-folder-id",
74 | },
75 | ],
76 | createdAt: new Date().toISOString(),
77 | updatedAt: new Date().toISOString(),
78 | expirationAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
79 | };
80 |
81 | beforeEach(() => {
82 | vi.clearAllMocks();
83 | (axios.post as Mock).mockResolvedValue({ data: mockCreateSendLinksResponse });
84 | });
85 |
86 | describe("upload.service", () => {
87 | it("When the uploadFiles() is done, then should call storeSendLinks() and ensure getCaptchaToken() is called once", async () => {
88 | const uploadFilesSpy = vi.spyOn(UploadService, "uploadFiles");
89 | const storeSendLinksSpy = vi.spyOn(UploadService, "storeSendLinks");
90 | (getCaptchaToken as Mock).mockResolvedValue("mock-token");
91 |
92 | const axiosPostSpy = vi.spyOn(axios, "post");
93 |
94 | uploadFilesSpy.mockResolvedValue([mockSendLink, mockSendLinkFolder]);
95 |
96 | const result = await UploadService.uploadFilesAndGetLink([
97 | {
98 | id: "1",
99 | name: "file1.txt",
100 | type: "file",
101 | size: 100,
102 | file: {} as unknown as FileWithPath,
103 | } as unknown as SendItemData,
104 | {
105 | id: "2",
106 | name: "file2.txt",
107 | type: "file",
108 | size: 200,
109 | file: {} as unknown as FileWithPath,
110 | } as unknown as SendItemData,
111 | ]);
112 |
113 | expect(uploadFilesSpy).toHaveBeenCalledTimes(1);
114 | expect(getCaptchaToken).toHaveBeenCalledTimes(1);
115 | expect(storeSendLinksSpy).toHaveBeenCalledTimes(1);
116 | expect(storeSendLinksSpy).toHaveBeenCalledWith(
117 | expect.objectContaining({
118 | items: expect.any(Array),
119 | code: expect.any(String),
120 | mnemonic: expect.any(String),
121 | plainCode: expect.any(String),
122 | })
123 | );
124 | expect(axiosPostSpy).toHaveBeenCalledTimes(1);
125 | expect(uploadFilesSpy.mock.invocationCallOrder[0]).toBeLessThan(
126 | (getCaptchaToken as Mock).mock.invocationCallOrder[0]
127 | );
128 | expect((getCaptchaToken as Mock).mock.invocationCallOrder[0]).toBeLessThan(
129 | axiosPostSpy.mock.invocationCallOrder[0]
130 | );
131 | expect(result).toContain("/download/");
132 | expect(result).toContain("?code=");
133 | });
134 |
135 | it("When links are sent to be created, the captcha is generated before that", async () => {
136 | const storeSendLinksSpy = vi.spyOn(UploadService, "storeSendLinks");
137 | const axiosPostSpy = vi.spyOn(axios, "post");
138 | (getCaptchaToken as Mock).mockResolvedValue("mock-token");
139 | vi.spyOn(UploadService, "uploadFiles").mockResolvedValue([mockSendLink]);
140 |
141 | await UploadService.uploadFilesAndGetLink([
142 | {
143 | id: "1",
144 | name: "file1.txt",
145 | type: "file",
146 | size: 100,
147 | file: {} as unknown as FileWithPath,
148 | } as unknown as SendItemData,
149 | ]);
150 |
151 | expect(getCaptchaToken).toHaveBeenCalled();
152 | expect(storeSendLinksSpy).toHaveBeenCalled();
153 | expect(storeSendLinksSpy.mock.invocationCallOrder[0]).toBeLessThan(
154 | (getCaptchaToken as Mock).mock.invocationCallOrder[0]
155 | );
156 | expect((getCaptchaToken as Mock).mock.invocationCallOrder[0]).toBeLessThan(
157 | axiosPostSpy.mock.invocationCallOrder[0]
158 | );
159 | });
160 |
161 | it("When an error is thrown before getCaptchaToken, then getCaptchaToken is not called", async () => {
162 | const uploadFilesSpy = vi
163 | .spyOn(UploadService, "uploadFiles")
164 | .mockImplementation(() => {
165 | throw new Error("Error before captcha");
166 | });
167 |
168 | await expect(
169 | UploadService.uploadFilesAndGetLink([
170 | {
171 | id: "1",
172 | name: "file1.txt",
173 | type: "file",
174 | size: 100,
175 | file: {} as unknown as FileWithPath,
176 | } as unknown as SendItemData,
177 | ])
178 | ).rejects.toThrow("Error before captcha");
179 |
180 | expect(uploadFilesSpy).toHaveBeenCalled();
181 | expect(UploadService.storeSendLinks).not.toHaveBeenCalled();
182 | expect(getCaptchaToken).not.toHaveBeenCalled();
183 | });
184 |
185 | it("When an error is thrown in getCaptchaToken, then storeSendLinks is not called", async () => {
186 | vi.spyOn(UploadService, "uploadFiles").mockResolvedValue([mockSendLink]);
187 | (getCaptchaToken as Mock).mockRejectedValue(new Error("Captcha error"));
188 | const axiosPostSpy = vi.spyOn(axios, "post");
189 |
190 | await expect(UploadService.storeSendLinks).rejects.toThrow("Captcha error");
191 |
192 | expect(getCaptchaToken).toHaveBeenCalled();
193 | expect(axiosPostSpy).not.toHaveBeenCalled();
194 | });
195 | });
196 |
--------------------------------------------------------------------------------
/src/services/zip/FlatFolderZip.ts:
--------------------------------------------------------------------------------
1 | import streamSaver from 'streamsaver';
2 | import fileDownload from 'js-file-download';
3 |
4 | import { binaryStreamToBlob, buildProgressStream } from '../../network/streams';
5 | import { loadWritableStreamPonyfill, createFolderWithFilesWritable, ZipStream } from "./Zip";
6 |
7 | type FlatFolderZipOpts = {
8 | abortController?: AbortController;
9 | progress?: (loadedBytes: number) => void;
10 | }
11 | function isBrave() {
12 | const maybeBrave = (window.navigator as { brave?: any }).brave;
13 |
14 | return maybeBrave !== undefined && maybeBrave.isBrave.name === "isBrave";
15 | }
16 | export class FlatFolderZip {
17 | private finished!: Promise;
18 | private zip: ZipStream;
19 | private passThrough: ReadableStream;
20 | private folderName: string;
21 | private abortController?: AbortController;
22 |
23 | constructor(folderName: string, opts: FlatFolderZipOpts) {
24 | this.folderName = folderName;
25 | this.zip = createFolderWithFilesWritable();
26 | this.abortController = opts.abortController;
27 |
28 | this.passThrough = opts.progress ?
29 | buildProgressStream(this.zip.stream, opts.progress) :
30 | this.zip.stream;
31 |
32 | const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
33 |
34 | if (isBrave()) return;
35 |
36 | if (isFirefox) {
37 | loadWritableStreamPonyfill().then(() => {
38 | streamSaver.WritableStream = window.WritableStream;
39 |
40 | this.finished = this.passThrough.pipeTo(
41 | streamSaver.createWriteStream(folderName + '.zip'),
42 | { signal: opts.abortController?.signal }
43 | );
44 | });
45 | } else {
46 | this.finished = this.passThrough.pipeTo(
47 | streamSaver.createWriteStream(folderName + '.zip'),
48 | { signal: opts.abortController?.signal }
49 | );
50 | }
51 | }
52 |
53 | addFile(name: string, source: ReadableStream): void {
54 | if (this.abortController?.signal.aborted) return;
55 |
56 | this.zip.addFile(name, source);
57 | }
58 |
59 | addFolder(name: string): void {
60 | if (this.abortController?.signal.aborted) return;
61 |
62 | this.zip.addFolder(name);
63 | }
64 |
65 | async close(): Promise {
66 | if (this.abortController?.signal.aborted) return;
67 |
68 | this.zip.end();
69 |
70 | if (isBrave()) {
71 | console.log('is Brave');
72 | return fileDownload(
73 | await binaryStreamToBlob(this.passThrough),
74 | `${this.folderName}.zip`,
75 | 'application/zip'
76 | );
77 | }
78 |
79 | await this.finished;
80 | }
81 |
82 | abort(): void {
83 | this.abortController?.abort();
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/styles/fonts/InstrumentSans/InstrumentSans-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/src/styles/fonts/InstrumentSans/InstrumentSans-Bold.woff2
--------------------------------------------------------------------------------
/src/styles/fonts/InstrumentSans/InstrumentSans-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/src/styles/fonts/InstrumentSans/InstrumentSans-Medium.woff2
--------------------------------------------------------------------------------
/src/styles/fonts/InstrumentSans/InstrumentSans-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/src/styles/fonts/InstrumentSans/InstrumentSans-Regular.woff2
--------------------------------------------------------------------------------
/src/styles/fonts/InstrumentSans/InstrumentSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/src/styles/fonts/InstrumentSans/InstrumentSans-SemiBold.woff2
--------------------------------------------------------------------------------
/src/styles/fonts/InstrumentSans/InstrumentSans[wdth,wght].woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/internxt/send-web/833a65279e0095dc4b7ce70e66b80389247f9a9a/src/styles/fonts/InstrumentSans/InstrumentSans[wdth,wght].woff2
--------------------------------------------------------------------------------
/src/styles/fonts/InstrumentSans/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans)
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/src/styles/fonts/InstrumentSans/font.css:
--------------------------------------------------------------------------------
1 | /* Instrument Sans font family */
2 |
3 | @font-face {
4 | font-family: 'Instrument Sans';
5 | font-style: normal;
6 | font-display: swap;
7 | src: local('Instrument Sans'), url('./InstrumentSans[wdth\,wght].woff2') format('woff2');
8 | }
9 |
10 | /* @font-face {
11 | font-family: 'Instrument Sans';
12 | font-weight: 400;
13 | font-style: normal;
14 | font-display: swap;
15 | src: local('Instrument Sans'), url('./InstrumentSans-Regular.woff2') format('woff2');
16 | }
17 |
18 | @font-face {
19 | font-family: 'Instrument Sans';
20 | font-weight: 500;
21 | font-style: normal;
22 | font-display: swap;
23 | src: local('Instrument Sans'), url('./InstrumentSans-Medium.woff2') format('woff2');
24 | }
25 |
26 | @font-face {
27 | font-family: 'Instrument Sans';
28 | font-weight: 600;
29 | font-style: normal;
30 | font-display: swap;
31 | src: local('Instrument Sans'), url('./InstrumentSans-SemiBold.woff2') format('woff2');
32 | }
33 |
34 | @font-face {
35 | font-family: 'Instrument Sans';
36 | font-weight: 700;
37 | font-style: normal;
38 | font-display: swap;
39 | src: local('Instrument Sans'), url('./InstrumentSans-Bold.woff2') format('woff2');
40 | } */
41 |
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @import url("./fonts/InstrumentSans/font.css");
6 |
7 | .fade-in-animation {
8 | animation-name: FadeIn;
9 | animation-duration: 500ms;
10 | transition-timing-function: ease-in-out;
11 | }
12 |
13 | iframe {
14 | overflow: hidden;
15 | height: 0;
16 | }
17 |
18 | @keyframes FadeIn {
19 | 0% {
20 | opacity: 0;
21 | }
22 |
23 | 100% {
24 | opacity: 1;
25 | }
26 | }
27 |
28 | html,
29 | body {
30 | background: #ffffff;
31 | background-color: #ffffff;
32 | }
33 |
34 | .grecaptcha-badge {
35 | visibility: hidden !important;
36 | }
37 |
38 | .dropdown {
39 | top: -90px;
40 | left: 20px;
41 | }
42 |
43 | .revealY {
44 | will-change: transform;
45 | transform: translateY(150px);
46 | opacity: 0;
47 | transition: 1s cubic-bezier(0.21, 1.23, 1, 0.97);
48 | }
49 |
50 | .revealY.active {
51 | transform: translateY(0);
52 | opacity: 1;
53 | }
54 |
55 | .revealXRight {
56 | will-change: transform;
57 | transform: translateX(-150px);
58 | opacity: 0;
59 | transition: 1s cubic-bezier(0.21, 1.23, 1, 0.97);
60 | }
61 |
62 | .revealXRight.active {
63 | transform: translateX(0);
64 | opacity: 1;
65 | }
66 |
67 | .revealXLeft {
68 | will-change: transform;
69 | transform: translateX(150px);
70 | opacity: 0;
71 | transition: 1s cubic-bezier(0.21, 1.23, 1, 0.97);
72 | }
73 |
74 | .revealXLeft.active {
75 | transform: translateX(0);
76 | opacity: 1;
77 | }
78 |
--------------------------------------------------------------------------------
/src/views/NotFoundView.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useNavigate } from "react-router-dom";
3 |
4 | export default function NotFoundView({
5 | className = "",
6 | }: {
7 | className?: string;
8 | }) {
9 | const navigate = useNavigate();
10 |
11 | useEffect(() => {
12 | navigate("/");
13 | }, []);
14 |
15 | return ;
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/vitest.config.js:
--------------------------------------------------------------------------------
1 | // vitest.config.ts
2 | import replace from "@rollup/plugin-replace";
3 | import react from "@vitejs/plugin-react";
4 | import path from "path";
5 | import { defineConfig } from "vitest/config";
6 |
7 | export default defineConfig({
8 | plugins: [
9 | react(),
10 | replace({
11 | preventAssignment: true,
12 | "process.browser": true,
13 | }),
14 | ],
15 | resolve: {
16 | alias: {
17 | // eslint-disable-next-line no-undef
18 | app: path.resolve(__dirname, "./src/app"),
19 | crypto: "crypto-browserify", // Resolve `crypto` to `crypto-browserify`
20 | stream: "stream-browserify",
21 | },
22 | },
23 | test: {
24 | environment: "jsdom",
25 | globals: true,
26 | setupFiles: "./src/setupTests.ts",
27 | exclude: ["node_modules", "dist"],
28 | include: [
29 | "src/**/*.test.{ts,tsx,js,jsx}",
30 | "test/unit/**/*.test.{ts,tsx,js,jsx}",
31 | ],
32 | // coverage: {
33 | // provider: "istanbul",
34 | // reporter: ["text", "lcov"],
35 | // reportsDirectory: "./coverage",
36 | // include: ["src/**/*.{js,ts,jsx,tsx}", "test/unit/**/*.{js,ts,jsx,tsx}"],
37 | // },
38 | },
39 | optimizeDeps: {
40 | esbuildOptions: {
41 | // Node.js global to browser globalThis
42 | define: {
43 | global: "globalThis",
44 | },
45 | },
46 | },
47 | });
48 |
--------------------------------------------------------------------------------