diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e02dae7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,78 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: Build Chrome & Firefox + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: yarn + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Type check + run: yarn compile + + - name: Build Chrome (MV3) + run: yarn build + env: + VITE_VPN_SERVER_ADDRESS: ${{ secrets.VITE_VPN_SERVER_ADDRESS }} + VITE_VPN_SERVER_PORT: ${{ secrets.VITE_VPN_SERVER_PORT }} + VITE_VPN_USERNAME: ${{ secrets.VITE_VPN_USERNAME }} + VITE_VPN_PASSWORD: ${{ secrets.VITE_VPN_PASSWORD }} + VITE_VPN_API_URL: ${{ secrets.VITE_VPN_API_URL }} + VITE_VPN_API_URL_STAGING: ${{ secrets.VITE_VPN_API_URL_STAGING }} + VITE_VPN_API_URL_DEVELOPMENT: ${{ secrets.VITE_VPN_API_URL_DEVELOPMENT }} + VITE_IP_API_URL: ${{ secrets.VITE_IP_API_URL }} + VITE_AUTH_HOST_URL: ${{ secrets.VITE_AUTH_HOST_URL }} + VITE_AUTH_HOST_URL_STAGING: ${{ secrets.VITE_AUTH_HOST_URL_STAGING }} + VITE_AUTH_HOST_URL_DEVELOPMENT: ${{ secrets.VITE_AUTH_HOST_URL_DEVELOPMENT }} + VITE_DRIVE_API_URL: ${{ secrets.VITE_DRIVE_API_URL }} + VITE_DRIVE_API_URL_STAGING: ${{ secrets.VITE_DRIVE_API_URL_STAGING }} + VITE_DRIVE_API_URL_DEVELOPMENT: ${{ secrets.VITE_DRIVE_API_URL_DEVELOPMENT }} + + - name: Build Firefox (MV2) + run: yarn build:firefox + env: + VITE_VPN_SERVER_ADDRESS: ${{ secrets.VITE_VPN_SERVER_ADDRESS }} + VITE_VPN_SERVER_PORT: ${{ secrets.VITE_VPN_SERVER_PORT }} + VITE_VPN_USERNAME: ${{ secrets.VITE_VPN_USERNAME }} + VITE_VPN_PASSWORD: ${{ secrets.VITE_VPN_PASSWORD }} + VITE_VPN_API_URL: ${{ secrets.VITE_VPN_API_URL }} + VITE_VPN_API_URL_STAGING: ${{ secrets.VITE_VPN_API_URL_STAGING }} + VITE_VPN_API_URL_DEVELOPMENT: ${{ secrets.VITE_VPN_API_URL_DEVELOPMENT }} + VITE_IP_API_URL: ${{ secrets.VITE_IP_API_URL }} + VITE_AUTH_HOST_URL: ${{ secrets.VITE_AUTH_HOST_URL }} + VITE_AUTH_HOST_URL_STAGING: ${{ secrets.VITE_AUTH_HOST_URL_STAGING }} + VITE_AUTH_HOST_URL_DEVELOPMENT: ${{ secrets.VITE_AUTH_HOST_URL_DEVELOPMENT }} + VITE_DRIVE_API_URL: ${{ secrets.VITE_DRIVE_API_URL }} + VITE_DRIVE_API_URL_STAGING: ${{ secrets.VITE_DRIVE_API_URL_STAGING }} + VITE_DRIVE_API_URL_DEVELOPMENT: ${{ secrets.VITE_DRIVE_API_URL_DEVELOPMENT }} + + - name: Upload Chrome build + uses: actions/upload-artifact@v4 + with: + name: chrome-mv3 + path: .output/chrome-mv3/ + retention-days: 7 + + - name: Upload Firefox build + uses: actions/upload-artifact@v4 + with: + name: firefox-mv2 + path: .output/firefox-mv2/ + retention-days: 7 diff --git a/src/entrypoints/background.ts b/src/entrypoints/background.ts index 0b77067..e9326a8 100644 --- a/src/entrypoints/background.ts +++ b/src/entrypoints/background.ts @@ -1,5 +1,6 @@ +import { browser } from 'wxt/browser' import { handleUserToken } from './utils/handleUserToken' -import { clearProxySettings } from './popup/proxy.service' +import { clearProxySettings, updateProxySettings } from './popup/proxy.service' const FOUR_DAYS_IN_MS = 4 * 24 * 60 * 60 * 1000 let interval: NodeJS.Timeout | null = null @@ -7,7 +8,6 @@ let interval: NodeJS.Timeout | null = null function startInterval() { if (interval) clearInterval(interval) interval = setInterval(() => { - console.log('Refresh token') handleUserToken() }, FOUR_DAYS_IN_MS) } @@ -15,13 +15,13 @@ function startInterval() { export default defineBackground(() => { const IP_API_URL = import.meta.env.VITE_IP_API_URL - chrome.runtime.onInstalled.addListener((details) => { + browser.runtime.onInstalled.addListener((details) => { if (details.reason === 'install') { - chrome.tabs.create({ url: 'https://internxt.com/vpn' }) + browser.tabs.create({ url: 'https://internxt.com/vpn' }) } }) - chrome.runtime.onMessage.addListener((message, _, sendResponse) => { + browser.runtime.onMessage.addListener((message, _, sendResponse) => { if (message === 'GET_DATA') { fetch(`${IP_API_URL}/json`, { method: 'GET', @@ -30,14 +30,21 @@ export default defineBackground(() => { .then((items) => { const { ip, city, region, country } = items const locationText = `${city}, ${region}, ${country}` - sendResponse({ location: locationText, ip, }) }) .catch(() => { - // NO OP + sendResponse(null) + }) + } else if (message === 'SET_PROXY') { + updateProxySettings() + .then(() => { + sendResponse({}) + }) + .catch(() => { + sendResponse({}) }) } else if (message === 'RESET_PROXY') { clearProxySettings() @@ -55,53 +62,72 @@ export default defineBackground(() => { const localCache = { token: null as string | null, connection: null as string | null, + vpnEnabled: false as boolean, } async function initializeLocalCache() { - const { userToken, connection } = await chrome.storage.local.get([ - 'userToken', - 'connection', - ]) + const result = await browser.storage.local.get(['userToken', 'connection', 'vpnEnabled']) + const userToken = result.userToken as { token: string } | undefined + const connection = result.connection as string | undefined console.log('INITIAL LOCAL STORAGE: ', userToken) localCache.token = userToken?.token ?? null localCache.connection = connection ?? null + localCache.vpnEnabled = (result.vpnEnabled as boolean) ?? false } startInterval() initializeLocalCache() - chrome.storage.onChanged.addListener((changes, areaName) => { + browser.storage.onChanged.addListener((changes, areaName) => { if (areaName === 'local') { if (changes.userToken?.newValue) { - localCache.token = changes.userToken.newValue?.token ?? null + localCache.token = (changes.userToken.newValue as { token: string })?.token ?? null startInterval() } if (changes.connection?.newValue) { - localCache.connection = changes.connection.newValue ?? null + localCache.connection = (changes.connection.newValue as string) ?? null + } + if ('vpnEnabled' in changes) { + localCache.vpnEnabled = (changes.vpnEnabled.newValue as boolean) ?? false } } }) - browser.webRequest.onAuthRequired.addListener( - function (details) { - if (details.isProxy) { - return { - authCredentials: { - username: localCache.connection ?? 'FR', - password: localCache.token ?? '', - }, - } - } - return {} - }, - { urls: [''] }, - ['blocking'] - ) + if (import.meta.env.BROWSER === 'firefox') { + const VPN_HOST = import.meta.env.VITE_VPN_SERVER_ADDRESS + const VPN_PORT = Number(import.meta.env.VITE_VPN_SERVER_PORT) + ;(browser as any).proxy.onRequest.addListener( + (details: any) => { + if (details.tabId === -1 || details.originUrl?.startsWith('moz-extension://')) return { type: 'direct' } + if (!localCache.vpnEnabled) return { type: 'direct' } + return { type: 'http', host: VPN_HOST, port: VPN_PORT, username: localCache.connection ?? 'FR', password: localCache.token ?? '' } + }, + { urls: [''] }, + ) - browser.webRequest.onErrorOccurred.addListener( - (error) => { - console.log('[AN ERROR OCURRED]:', error) - }, - { urls: [''] } - ) + browser.webRequest.onAuthRequired.addListener( + function (details) { + if (!details.isProxy) return {} + return { authCredentials: { username: localCache.connection ?? 'FR', password: localCache.token ?? '' } } + }, + { urls: [''] }, + ['blocking'], + ) + } else { + browser.webRequest.onAuthRequired.addListener( + function (details) { + if (details.isProxy) { + return { + authCredentials: { + username: localCache.connection ?? 'FR', + password: localCache.token ?? '', + }, + } + } + return {} + }, + { urls: [''] }, + ['blocking'], + ) + } }) diff --git a/src/entrypoints/components/RestartBrowserModal.tsx b/src/entrypoints/components/RestartBrowserModal.tsx new file mode 100644 index 0000000..14d9986 --- /dev/null +++ b/src/entrypoints/components/RestartBrowserModal.tsx @@ -0,0 +1,29 @@ +import { translate } from '@/constants' + +interface RestartBrowserModalProps { + onClose: () => void +} + +export const RestartBrowserModal = ({ onClose }: RestartBrowserModalProps) => { + return ( +
+
+
+

+ {translate('firefoxLocationModal.title')} +

+

+ {translate('firefoxLocationModal.description')} +

+
+ +
+
+ ) +} diff --git a/src/entrypoints/components/StatusComponent.tsx b/src/entrypoints/components/StatusComponent.tsx index fb7a118..df8a9b1 100644 --- a/src/entrypoints/components/StatusComponent.tsx +++ b/src/entrypoints/components/StatusComponent.tsx @@ -1,7 +1,7 @@ -import { VPN_STATUS_SWITCH } from '../constants' +import { VPN_STATUS } from '~/entrypoints/popup/App' interface StatusComponentProps { - status: VPN_STATUS_SWITCH + status: VPN_STATUS } export const StatusComponent = ({ status }: StatusComponentProps) => { diff --git a/src/entrypoints/components/dropdown/components/DropdownSection.tsx b/src/entrypoints/components/dropdown/components/DropdownSection.tsx index 5c0cc52..72ea7b7 100644 --- a/src/entrypoints/components/dropdown/components/DropdownSection.tsx +++ b/src/entrypoints/components/dropdown/components/DropdownSection.tsx @@ -1,3 +1,4 @@ +import { browser } from 'wxt/browser' import { translate } from '@/constants' import { SectionItemProps, SectionProps } from '../Dropdown' import { DropdownItem } from './DropdownItem' @@ -16,7 +17,7 @@ export const DropdownSection = ({ onItemClicked, }: DropdownSectionProps) => { const handleUpgradeButtonClicked = () => { - chrome.tabs.create({ url: 'https://internxt.com/pricing' }) + browser.tabs.create({ url: 'https://internxt.com/pricing' }) } return (
{ - console.log( - 'The user has been authenticated in the VPN extension', - ) - }, - ) + browser.storage.local.set({ userToken: { token, type: 'user' } }) } else if (eventMessage === MESSAGES.USER_LOG_OUT) { - chrome.storage.local.clear(async () => { - await chrome.runtime.sendMessage('RESET_PROXY') - console.log('The user has been logged out from the VPN extension') + browser.storage.local.clear().then(async () => { + await browser.runtime.sendMessage('RESET_PROXY') }) } } diff --git a/src/entrypoints/popup/App.tsx b/src/entrypoints/popup/App.tsx index de10f3a..9ef1999 100644 --- a/src/entrypoints/popup/App.tsx +++ b/src/entrypoints/popup/App.tsx @@ -1,10 +1,15 @@ import { useEffect, useState } from 'react' +import { browser } from 'wxt/browser' -import { clearProxySettings, updateProxySettings } from './proxy.service' +import { clearProxySettings } from './proxy.service' import { ConnectionDetails } from '../components/ConnectionDetails' import { VpnStatus } from '../components/VpnStatus' import { Footer } from '../components/Footer' +import { RestartBrowserModal } from '../components/RestartBrowserModal' import { translate } from '@/constants' + +const IS_FIREFOX = import.meta.env.BROWSER === 'firefox' + import { getAnonymousToken, getUserAvailableLocations, @@ -44,6 +49,8 @@ export const App = () => { const [availableLocations, setAvailableLocations] = useState([ 'FR', ]) + const [showFirefoxLocationInfo, setShowFirefoxLocationInfo] = useState(false) + const [firefoxNeedsRestart, setFirefoxNeedsRestart] = useState(false) useEffect(() => { initialAppState() @@ -51,7 +58,7 @@ export const App = () => { const initialAppState = async () => { try { - const storageData = (await chrome.storage.local.get([ + const storageData = (await browser.storage.local.get([ 'vpnStatus', 'userData', 'userToken', @@ -81,9 +88,7 @@ export const App = () => { setSelectedLocation(location) } catch (error) { - console.error(`ERROR WHILE INITIALIZING APP STATE: ${error}`) if (error instanceof UnauthorizedError) { - console.warn('Authorization error detected:', error.message) await onLogOut() } } @@ -99,11 +104,11 @@ export const App = () => { } const onConnectVpn = async () => { - await updateProxySettings() - const userData = await chrome.runtime.sendMessage('GET_DATA') - setUserData(userData) - await storageService.saveVpnStatus('ON', userData) - + await browser.runtime.sendMessage('SET_PROXY') + const userData = await browser.runtime.sendMessage('GET_DATA') + const resolvedUserData = userData ?? defaultUserDataInfo + setUserData(resolvedUserData) + await storageService.saveVpnStatus('ON', resolvedUserData) setStatus('ON') } @@ -115,6 +120,10 @@ export const App = () => { } const onToggleClicked = async () => { + if (IS_FIREFOX && firefoxNeedsRestart && status === 'OFF') { + setShowFirefoxLocationInfo(true) + return + } setStatus('CONNECTING') try { if (status === 'OFF') { @@ -124,9 +133,7 @@ export const App = () => { } } catch (err) { await onDisconnectVpn() - } finally { - const newStatus = status === 'OFF' ? 'ON' : 'OFF' - setStatus(newStatus) + setStatus('OFF') } } @@ -147,6 +154,14 @@ export const App = () => { } const onChangeLocation = async (newLocation: VPNLocation) => { + if (IS_FIREFOX && status === 'ON') { + if (newLocation !== selectedLocation) { + setShowFirefoxLocationInfo(true) + setFirefoxNeedsRestart(true) + } + return + } + try { if (status === 'ON') { await onDisconnectVpn() @@ -218,7 +233,7 @@ export const App = () => { const dropdownSections = getDropdownSections(availableLocations) return ( -
+
{/* Main section (logo, title, description) */}
{
+ {showFirefoxLocationInfo && ( + setShowFirefoxLocationInfo(false)} /> + )}
) } diff --git a/src/entrypoints/popup/proxy.service.ts b/src/entrypoints/popup/proxy.service.ts index 5a4d203..5e1ffb8 100644 --- a/src/entrypoints/popup/proxy.service.ts +++ b/src/entrypoints/popup/proxy.service.ts @@ -5,25 +5,23 @@ const VPN_CONFIG = { PORT: Number(import.meta.env.VITE_VPN_SERVER_PORT), } +const IS_FIREFOX = import.meta.env.BROWSER === 'firefox' + async function clearProxyCache() { - const options: Record = {} - const rootDomain = VPN_CONFIG.HOST - options.origins = [] - options.origins.push('http://' + rootDomain) - options.origins.push('https://' + rootDomain) - - const types = { cookies: true } - chrome.browsingData.remove(options, types, function () { - console.log('PROXY CACHE REMOVED') - }) + browser.browsingData.remove({}, { cookies: true }) } export async function updateProxySettings() { + if (IS_FIREFOX) { + await browser.storage.local.set({ vpnEnabled: true }) + return + } + const proxyConfig = { - mode: 'fixed_servers', + mode: 'fixed_servers' as const, rules: { singleProxy: { - scheme: 'http', + scheme: 'http' as const, host: VPN_CONFIG.HOST, port: VPN_CONFIG.PORT, }, @@ -31,30 +29,21 @@ export async function updateProxySettings() { }, } - browser.proxy.settings - .set({ value: proxyConfig, scope: 'regular' }) - .then(() => { - console.log('CONNECTED') - }) - .catch((err) => { - console.log('ERROR WHILE CONNECTING TO THE PROXY: ', err) - }) + browser.proxy.settings.set({ value: proxyConfig, scope: 'regular' }) } export async function clearProxySettings() { + if (IS_FIREFOX) { + await browser.storage.local.set({ vpnEnabled: false }) + clearProxyCache() + return + } + const proxyConfig = { - mode: 'system', + mode: 'system' as const, } - browser.proxy.settings - .set({ value: proxyConfig, scope: 'regular' }) - .then(() => { - if (browser.runtime.lastError) { - console.error( - 'ERROR ADDING THE DEFAULT PROXY CONFIG: ', - browser.runtime.lastError, - ) - } - clearProxyCache() - }) + browser.proxy.settings.set({ value: proxyConfig, scope: 'regular' }).then(() => { + clearProxyCache() + }) } diff --git a/src/entrypoints/services/storage.service.ts b/src/entrypoints/services/storage.service.ts index 9ccd3f6..b71e43d 100644 --- a/src/entrypoints/services/storage.service.ts +++ b/src/entrypoints/services/storage.service.ts @@ -1,3 +1,4 @@ +import { browser } from 'wxt/browser' import { UserData, VPN_STATUS, VPNLocation } from '../popup/App' type TokenType = 'anonymous' | 'user' @@ -6,7 +7,7 @@ export const saveUserToken = async ( tokenType: TokenType, userToken: string ) => { - await chrome.storage.local.set({ + await browser.storage.local.set({ userToken: { token: userToken, type: tokenType, @@ -18,12 +19,12 @@ export const getUserToken = async (): Promise<{ token: string type: TokenType }> => { - const storageData = await chrome.storage.local.get('userToken') + const storageData = await browser.storage.local.get('userToken') return storageData.userToken as { token: string; type: TokenType } } export const saveUserConnection = async (connection: VPNLocation) => { - await chrome.storage.local.set({ + await browser.storage.local.set({ connection, }) } @@ -32,7 +33,7 @@ export const saveVpnStatus = async ( vpnStatus: VPN_STATUS, userData: UserData ) => { - await chrome.storage.local.set({ + await browser.storage.local.set({ vpnStatus, userData, }) diff --git a/src/entrypoints/utils/handleUserToken.ts b/src/entrypoints/utils/handleUserToken.ts index 744f6d4..8f1f642 100644 --- a/src/entrypoints/utils/handleUserToken.ts +++ b/src/entrypoints/utils/handleUserToken.ts @@ -7,14 +7,12 @@ import storageService, { getUserToken } from '../services/storage.service' const refreshExistentUserToken = async (userToken: string) => { const refreshedToken = await refreshUserToken(userToken) - console.log(`User token refreshed`) - await storageService.saveUserToken('user', refreshedToken) + await storageService.saveUserToken('user', refreshedToken) } const refreshAnonymousToken = async () => { const anonymousToken = await getAnonymousToken() - console.log(`Anonymous token refreshed`) - await storageService.saveUserToken('anonymous', anonymousToken.token) + await storageService.saveUserToken('anonymous', anonymousToken.token) } export const handleUserToken = async () => { diff --git a/src/locales/en.json b/src/locales/en.json index 35f1453..fe18fa8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -32,6 +32,11 @@ "description": "Connect for secure browsing." } }, + "firefoxLocationModal": { + "title": "Restart browser to change location", + "description": "Close Firefox > Open a new window > Open Internxt VPN > Click preferred location", + "confirm": "Got it" + }, "footer": { "login": "Log in", "signup": "Sign up", diff --git a/wxt.config.ts b/wxt.config.ts index b992cba..09d68d4 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ }), modules: ['@wxt-dev/i18n/module'], srcDir: 'src', - manifest: { + manifest: ({ browser }) => ({ name: 'Internxt VPN - Free, Encrypted & Unlimited VPN', short_name: 'Internxt VPN', default_locale: 'en', @@ -25,7 +25,9 @@ export default defineConfig({ 'storage', 'proxy', 'webRequest', - 'webRequestAuthProvider', + ...(browser === 'firefox' + ? ['webRequestBlocking'] + : ['webRequestAuthProvider']), 'browsingData', ], web_accessible_resources: [ @@ -38,5 +40,5 @@ export default defineConfig({ action: { default_popup: 'index.html', }, - }, + }), })