Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
VITE_PAYMENTS_API_URL=
VITE_DRIVE_API_URL=
VITE_MAGIC_IV=
VITE_MAGIC_SALT=
VITE_CRYPTO_SECRET=
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Mail Web CI

on:
pull_request:
branches: [master]

jobs:
ci:
runs-on: ubuntu-22.04

strategy:
matrix:
node-version: [24.x]

env:
VITE_PAYMENTS_API_URL: ${{ secrets.PAYMENTS_API_URL }}
VITE_DRIVE_API_URL: ${{ secrets.DRIVE_API_URL }}
VITE_MAGIC_IV: ${{ secrets.MAGIC_IV }}
VITE_MAGIC_SALT: ${{ secrets.MAGIC_SALT }}
VITE_CRYPTO_SECRET: ${{ secrets.CRYPTO_SECRET }}

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "yarn"

- run: yarn install --frozen-lockfile
- run: yarn build

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it makes more sense to run the build before the tests 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmmm, yeah, could be.

- run: yarn test
13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@
"private": true,
"version": "0.0.0",
"type": "module",
"engines": {
"node": ">=24"
},
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@internxt/css-config": "^1.1.0",
"@internxt/sdk": "^1.15.1",
"@internxt/ui": "^0.1.9",
"@phosphor-icons/react": "^2.1.10",
"@tailwindcss/vite": "^4.2.1",
Expand Down Expand Up @@ -41,9 +48,11 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"jsdom": "^28.1.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0",
"vite": "^7.3.1",
"vite-plugin-node-polyfills": "^0.25.0"
"vite-plugin-node-polyfills": "^0.25.0",
"vitest": "^4.0.18"
}
}
2 changes: 1 addition & 1 deletion src/components/chips/RecipientChip.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { XIcon } from '@phosphor-icons/react'
import type { Recipient } from '../composeMessageDialog/types'
import type { Recipient } from '../mail/composeMessageDialog/types'

interface RecipientChipProps {
recipient: Recipient
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, type KeyboardEvent } from 'react'
import type { Recipient } from '../types'
import { RecipientChip } from '../../chips/RecipientChip'
import { RecipientChip } from '@/components/chips/RecipientChip'

interface RecipientInputProps {
label: string
Expand Down Expand Up @@ -46,7 +46,7 @@ export const RecipientInput = ({
inputValue === '' &&
recipients.length > 0
) {
onRemoveRecipient(recipients[recipients.length - 1].id)
onRemoveRecipient(recipients.at(-1)!.id)
}
}

Expand All @@ -60,9 +60,7 @@ export const RecipientInput = ({

return (
<div className="flex flex-row gap-2 items-start">
<p className="font-medium max-w-[64px] w-full text-gray-100 py-2">
{label}
</p>
<p className="font-medium max-w-16 w-full text-gray-100 py-2">{label}</p>
<div className="flex-1 flex items-center gap-1 flex-wrap rounded-lg border border-gray-10 bg-surface px-3 py-1.5 focus-within:border-primary focus-within:ring-1 focus-within:ring-primary">
{recipients.map((recipient) => (
<RecipientChip
Expand All @@ -78,7 +76,7 @@ export const RecipientInput = ({
onKeyDown={handleKeyDown}
onBlur={handleBlur}
disabled={disabled}
className={`flex-1 min-w-[120px] bg-transparent text-sm text-gray-100 placeholder:text-gray-40 focus:outline-none py-0.5 ${disabled ? 'cursor-not-allowed' : ''}`}
className={`flex-1 min-w-30 bg-transparent text-sm text-gray-100 placeholder:text-gray-40 focus:outline-none py-0.5 ${disabled ? 'cursor-not-allowed' : ''}`}
/>
{showCcBcc && (showCcButton || showBccButton) && (
<div className="flex items-center gap-1 ml-auto shrink-0">
Expand All @@ -87,7 +85,7 @@ export const RecipientInput = ({
type="button"
onClick={onCcClick}
disabled={disabled}
className={`px-1.5 py-0.5 text-sm font-medium text-primary hover:bg-gray-5 rounded bg-primary/20 hover:bg-primary/30 ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
className={`px-1.5 py-0.5 text-sm font-medium text-primary rounded bg-primary/20 hover:bg-primary/30 ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{ccButtonText}
</button>
Expand All @@ -97,7 +95,7 @@ export const RecipientInput = ({
type="button"
onClick={onBccClick}
disabled={disabled}
className={`px-1.5 py-0.5 text-sm font-medium text-primary hover:bg-gray-5 rounded bg-primary/20 hover:bg-primary/30 ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
className={`px-1.5 py-0.5 text-sm font-medium text-primary rounded bg-primary/20 hover:bg-primary/30 ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{bccButtonText}
</button>
Expand Down
7 changes: 7 additions & 0 deletions src/services/config/config.errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class VariableNotFoundError extends Error {
constructor(variableName: string) {
super(`Variable not found: ${variableName}`)

Object.setPrototypeOf(this, VariableNotFoundError.prototype)
}
}
48 changes: 48 additions & 0 deletions src/services/config/config.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, vi, beforeEach, test, afterEach } from 'vitest'
import { VariableNotFoundError } from './config.errors'

import { ConfigService } from '.'
describe('Config Service', () => {
beforeEach(() => {
vi.stubEnv('VITE_DRIVE_API_URL', 'https://api-drive.internxt.com')
vi.stubEnv('VITE_MAIL_API_URL', 'https://api-mail.internxt.com')
vi.stubEnv('VITE_PAYMENTS_API_URL', 'https://api-payments.internxt.com')
vi.stubEnv('VITE_CRYPTO_SECRET', 'test-secret')
vi.stubEnv('VITE_MAGIC_IV', 'test-iv')
vi.stubEnv('VITE_MAGIC_SALT', 'test-salt')
vi.stubEnv('PROD', false)
})

afterEach(() => {
vi.unstubAllEnvs()
})

const configService = ConfigService.instance

describe('Get Variable', () => {
test('When getting an existing variable, then it should be returned successfully', () => {
const result = configService.getVariable('DRIVE_API_URL')
expect(result).toBe('https://api-drive.internxt.com')
})

test('When the variable does not exist, then an error indicating so is thrown', () => {
vi.stubEnv('VITE_DRIVE_API_URL', undefined)

expect(() => configService.getVariable('DRIVE_API_URL')).toThrow(
VariableNotFoundError,
)
})
})

describe('Checking if the environment is production', () => {
test('When the environment is not production, then should indicate so', () => {
expect(configService.isProduction()).toBe(false)
})

test('When the environment is production, then should indicate so', () => {
vi.stubEnv('PROD', true)

expect(configService.isProduction()).toBe(true)
})
})
})
33 changes: 33 additions & 0 deletions src/services/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { VariableNotFoundError } from './config.errors'

interface ConfigKeys {
DRIVE_API_URL: string
MAIL_API_URL: string
PAYMENTS_API_URL: string
CRYPTO_SECRET: string
MAGIC_IV: string
MAGIC_SALT: string
}

const configKeys: Record<keyof ConfigKeys, string> = {
DRIVE_API_URL: 'VITE_DRIVE_API_URL',
MAIL_API_URL: 'VITE_MAIL_API_URL',
PAYMENTS_API_URL: 'VITE_PAYMENTS_API_URL',
CRYPTO_SECRET: 'VITE_CRYPTO_SECRET',
MAGIC_IV: 'VITE_MAGIC_IV',
MAGIC_SALT: 'VITE_MAGIC_SALT',
}

export class ConfigService {
public static readonly instance: ConfigService = new ConfigService()

public getVariable = (key: keyof ConfigKeys): string => {
const value = import.meta.env[configKeys[key]]
if (!value) throw new VariableNotFoundError(key)
return value
}

public isProduction = (): boolean => {
return import.meta.env.PROD
}
}
74 changes: 74 additions & 0 deletions src/services/local-storage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { UserSubscription } from '@internxt/sdk/dist/drive/payments/types/types'
import type { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'

const LocalStorageKeys = {
xUser: 'xUser',
xNewToken: 'xNewToken',
}

export class LocalStorageService {
public static readonly instance = new LocalStorageService()

set(key: string, value: string) {
localStorage.setItem(key, value)
}

get(key: string): string | null {
return localStorage.getItem(key)
}

remove(key: string) {
localStorage.removeItem(key)
}

clear() {
localStorage.clear()
}

setUser(user: UserSettings) {
localStorage.setItem(LocalStorageKeys.xUser, JSON.stringify(user))
}

getUser(): UserSettings | null {
const user = localStorage.getItem(LocalStorageKeys.xUser)
return user ? JSON.parse(user) : null
}

setToken(token: string) {
localStorage.setItem(LocalStorageKeys.xNewToken, token)
}

getToken(): string | null {
return localStorage.getItem(LocalStorageKeys.xNewToken)
}

setMnemonic(mnemonic: string) {
localStorage.setItem('xMnemonic', mnemonic)
}

getMnemonic(): string | null {
return localStorage.getItem('xMnemonic')
}

setSubscription(subscription: UserSubscription) {
localStorage.setItem('xSubscription', JSON.stringify(subscription))
}

getSubscription(): UserSubscription | null {
const subscription = localStorage.getItem('xSubscription')
return subscription ? JSON.parse(subscription) : null
}

saveCredentials(user: UserSettings, mnemonic: string, token: string) {
this.setUser(user)
this.setMnemonic(mnemonic)
this.setToken(token)
}

clearCredentials() {
localStorage.removeItem('xUser')
localStorage.removeItem('xNewToken')
localStorage.removeItem('xSubscription')
localStorage.removeItem('xMnemonic')
}
}
Loading