Skip to content

Commit d9ad9c7

Browse files
committed
feat: basic auth persist
1 parent b369463 commit d9ad9c7

17 files changed

+160
-48
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"build": "cross-env NODE_ENV=production run-s dist type:check 'vite:build {@}' --",
4141
"build:web": "VITE_WEB=true VITE_BASE=/trakt-extension/ pnpm run build",
4242
"analyse": "vite-bundle-visualizer",
43-
"test:unit": "vitest run --environment jsdom --coverage --passWithNoTests",
43+
"test:unit": "vitest run --environment jsdom --coverage",
4444
"test:watch": "vitest --environment jsdom",
4545
"typecheck": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
4646
"lint": "eslint .",

src/components/AppComponent.vue

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import { RouterView } from 'vue-router';
33
44
import { NavbarComponent } from '~/components/common';
5-
import { useSettingsStore } from '~/stores/settings.store';
5+
import { useSettingsStoreRefs } from '~/stores/settings.store';
66
import { useI18n } from '~/utils';
77
88
const i18n = useI18n('global');
9-
const { isAuthenticated } = useSettingsStore();
9+
const { isAuthenticated } = useSettingsStoreRefs();
1010
</script>
1111

1212
<template>
@@ -29,7 +29,6 @@ main {
2929
display: flex;
3030
align-items: center;
3131
justify-content: center;
32-
height: 100%;
3332
padding: 0 2rem;
3433
}
3534
</style>

src/components/views/login/LoginComponent.vue

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { isLoginAuthResponseSuccess } from '~/models/login/login-auth-response';
1313
1414
import { traktService } from '~/services/trakt-client/trakt-client.service';
1515
import { useI18n } from '~/utils';
16+
import { createTab } from '~/utils/browser/browser.utils';
1617
1718
type LoginQueryParams = {
1819
redirect: string;
@@ -38,7 +39,7 @@ const auth = ref<TraktDeviceAuthentication>();
3839
const redirect = async () => {
3940
try {
4041
const response = await traktService.redirectToAuthentication();
41-
await chrome.tabs.create({ url: response.url });
42+
await createTab({ url: response.url });
4243
} catch (error) {
4344
console.error('Error:', error);
4445
}

src/services/trakt-client/clients/base-trakt-client.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@ import { describe, expect, it } from 'vitest';
33
import { TraktApiHeaders } from '../../../models/trakt/trakt-client.model';
44

55
import { CancellableFetch } from '../../../utils/fetch.utils';
6-
import { traktClientSettings } from '../trakt-client.service';
6+
7+
import { traktClientSettings } from '../trakt-client.config';
78

89
import { BaseTraktClient, parseBody, parseResponse } from './base-trakt-client';
910

1011
import type { TraktClientAuthentication } from '../../../models/trakt/trakt-authentication.model';
1112
import type { TraktApiInit, TraktApiParams, TraktApiQuery, TraktApiResponse, TraktApiTemplate } from '../../../models/trakt/trakt-client.model';
13+
1214
import type { CacheStore } from '../../../utils/cache.utils';
1315
import type { CancellablePromise } from '../../../utils/fetch.utils';
16+
1417
import type { Updater } from '../../../utils/observable.utils';
1518

1619
class TestableTraktClient extends BaseTraktClient {

src/services/trakt-client/clients/trakt-client.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { TraktClientEndpoint } from '../../../models/trakt/trakt-client.model';
44

55
import { CancellableFetch } from '../../../utils/fetch.utils';
66
import { traktApi } from '../api/trakt-api.endpoints';
7-
import { traktClientSettings } from '../trakt-client.service';
7+
8+
import { traktClientSettings } from '../trakt-client.config';
89

910
import { TraktClient } from './trakt-client';
1011

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { TraktClientSettings } from '~/models/trakt/trakt-client.model';
2+
3+
import { Config, Production, Staging } from '~/settings/traktv.api';
4+
5+
const isProd = import.meta.env.PROD;
6+
7+
const client = isProd ? Production : Staging;
8+
9+
export const traktClientSettings: TraktClientSettings = {
10+
client_id: client.ID,
11+
client_secret: client.Secret,
12+
redirect_uri: client.RedirectionUrl,
13+
endpoint: client.TraktEndpoint,
14+
15+
useragent: Config.UserAgent,
16+
};
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,4 @@
1-
import type { TraktClientSettings } from '~/models/trakt/trakt-client.model';
2-
31
import { TraktClient } from '~/services/trakt-client/clients/trakt-client';
4-
import { Config, Production, Staging } from '~/settings/traktv.api';
5-
6-
const isProd = import.meta.env.PROD;
7-
8-
const client = isProd ? Production : Staging;
9-
10-
export const traktClientSettings: TraktClientSettings = {
11-
client_id: client.ID,
12-
client_secret: client.Secret,
13-
redirect_uri: client.RedirectionUrl,
14-
endpoint: client.TraktEndpoint,
15-
16-
useragent: Config.UserAgent,
17-
};
2+
import { traktClientSettings } from '~/services/trakt-client/trakt-client.config';
183

194
export const traktService = new TraktClient(traktClientSettings);
20-
21-
traktService.onAuthChange(auth => {
22-
console.info('TraktClient.onAuthChange', auth);
23-
});
24-
25-
traktService.onCall(async call => {
26-
console.info('TraktClient.onCall', call);
27-
});

src/stores/i18n.store.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import { defineStore } from 'pinia';
1+
import { defineStore, storeToRefs } from 'pinia';
22
import { computed, ref } from 'vue';
33

4-
import type { ChromeI18nInput } from '@dvcol/web-extension-utils/lib/chrome/models';
5-
64
import type { Locale, Locales } from '~/models/i18n.model';
5+
import type { BrowserI18nInput } from '~/utils/browser/browser-i18n.utils';
76

87
export const useI18nStore = defineStore('i18n', () => {
98
const lang = ref<string>('en');
109
const locales = ref<Locales>({});
1110
const locale = computed<Locale>(() => locales.value[lang.value]);
1211

13-
const i18n = (value: string | ChromeI18nInput, ...modules: string[]) => {
12+
const i18n = (value: string | BrowserI18nInput, ...modules: string[]) => {
1413
const path: string = Array.isArray(modules) ? modules.join('__') : modules;
1514

1615
let key: string;
@@ -40,3 +39,5 @@ export const useI18nStore = defineStore('i18n', () => {
4039

4140
return { lang, locales, locale, i18n, addLocale };
4241
});
42+
43+
export const useI18nStoreRefs = () => storeToRefs(useI18nStore());

src/stores/router.store.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineStore } from 'pinia';
1+
import { defineStore, storeToRefs } from 'pinia';
22
import { ref } from 'vue';
33

44
export const useRouterStore = defineStore('router', () => {
@@ -20,3 +20,5 @@ export const useRouterStore = defineStore('router', () => {
2020

2121
return { baseName, setBaseName, baseUrl, setBaseUrl, initialLocation, routeParam, setRouteParam };
2222
});
23+
24+
export const useRouterStoreRefs = () => storeToRefs(useRouterStore());

src/stores/settings.store.ts

+27-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,39 @@
1-
import { defineStore } from 'pinia';
1+
import { defineStore, storeToRefs } from 'pinia';
22
import { ref } from 'vue';
33

44
import type { TraktClientAuthentication } from '~/models/trakt/trakt-authentication.model';
55

6+
import { storage } from '~/utils/browser/browser-storage.utils';
7+
68
export const useSettingsStore = defineStore('settings', () => {
79
const auth = ref<TraktClientAuthentication>();
8-
const setAuth = (_auth?: TraktClientAuthentication) => {
9-
auth.value = _auth;
10-
};
11-
1210
const isAuthenticated = ref(false);
11+
1312
const setAuthenticated = (authenticated: boolean = false) => {
1413
isAuthenticated.value = authenticated;
1514
};
1615

17-
return { auth, setAuth, isAuthenticated, setAuthenticated };
16+
const syncSetAuth = (_auth: TraktClientAuthentication) => storage.sync.set('settings.auth', _auth);
17+
const syncClearAuth = () => storage.sync.remove('settings.auth');
18+
const syncRestoreAuth = () =>
19+
storage.sync.get<TraktClientAuthentication>('settings.auth').then(_auth => {
20+
auth.value = _auth;
21+
setAuthenticated(!!_auth?.access_token);
22+
return _auth;
23+
});
24+
25+
const setAuth = (_auth?: TraktClientAuthentication) => {
26+
auth.value = _auth;
27+
setAuthenticated(!!_auth?.access_token);
28+
console.info('settings-store', 'Auth changed', _auth);
29+
if (_auth?.access_token) {
30+
syncSetAuth(_auth).then(() => console.info('settings-store', 'Auth saved', _auth));
31+
} else {
32+
syncClearAuth().then(() => console.info('settings-store', 'Auth cleared'));
33+
}
34+
};
35+
36+
return { auth, setAuth, isAuthenticated, setAuthenticated, syncRestoreAuth };
1837
});
38+
39+
export const useSettingsStoreRefs = () => storeToRefs(useSettingsStore());
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export type BrowserI18nInput = { key: string; substitutions: string[] };
2+
3+
/**
4+
* Convert translation using chrome i18n
5+
* @param value key string or object to translate
6+
* @param modules optionals modules names
7+
* @see chrome.i18n.getMessage
8+
* @see [chrome.i18n](https://developer.chrome.com/docs/extensions/reference/i18n/)
9+
*/
10+
export const i18nTranslate = (value: string | BrowserI18nInput, ...modules: string[]): string => {
11+
const path: string = Array.isArray(modules) ? modules.join('__') : modules;
12+
13+
let key: string;
14+
let substitution;
15+
if (typeof value === 'string') {
16+
key = path ? `${path}__${value}` : value;
17+
} else {
18+
key = path ? `${path}__${value.key}` : value.key;
19+
substitution = value?.substitutions;
20+
}
21+
return chrome?.i18n.getMessage?.(key, substitution) || key;
22+
};
23+
24+
/**
25+
* Setup i18n function with modules names
26+
* @param roots modules names
27+
* @see chrome.i18n.getMessage
28+
*/
29+
export const useI18nTranslate =
30+
(...roots: string[]): typeof i18nTranslate =>
31+
(value, ...modules): string =>
32+
i18nTranslate(value, ...(modules?.length ? modules : roots));
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* @see [chrome.storage.sync](https://developer.chrome.com/docs/extensions/reference/storage/#type-SyncStorageArea)
3+
*/
4+
export const syncStorage: chrome.storage.SyncStorageArea = chrome?.storage?.sync;
5+
6+
/**
7+
* @see [chrome.storage.local](https://developer.chrome.com/docs/extensions/reference/storage/#type-LocalStorageArea)
8+
*/
9+
export const localStorage: chrome.storage.LocalStorageArea = chrome?.storage?.local;
10+
11+
/**
12+
* @see [chrome.storage.session](https://developer.chrome.com/docs/extensions/reference/storage/#type-StorageArea)
13+
*/
14+
export const sessionStorage: chrome.storage.StorageArea = chrome?.storage?.session;
15+
16+
/**
17+
* This function is used to wrap the storage areas to provide type inference and a more convenient interface.
18+
* @param area The storage area to wrap.
19+
*/
20+
export const storageWrapper = (area: chrome.storage.StorageArea) => ({
21+
get: <T>(key: string): Promise<T> => area.get(key).then(({ [key]: value }) => value),
22+
set: <T>(key: string, value: T): Promise<void> => area.set({ [key]: value }),
23+
remove: (key: string): Promise<void> => area.remove(key),
24+
clear: (): Promise<void> => area.clear(),
25+
});
26+
27+
/**
28+
* This object is used to access the storage areas.
29+
*/
30+
export const storage = {
31+
sync: storageWrapper(syncStorage),
32+
local: storageWrapper(localStorage),
33+
session: storageWrapper(sessionStorage),
34+
};

src/utils/browser/browser.utils.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/**
2+
* @see [chrome.tabs.create](https://developer.chrome.com/docs/extensions/reference/tabs/#method-create)
3+
*/
4+
export const createTab = chrome?.tabs?.create;

src/utils/i18n.utils.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import * as WebExUtils from '@dvcol/web-extension-utils/lib/chrome/utils/i18n.utils';
21
import { afterEach, describe, expect, it, vi } from 'vitest';
32

43
import * as I18nStore from '../stores/i18n.store';
54

65
import { useI18n } from './i18n.utils';
76

87
import * as RouterStore from '~/stores/router.store';
8+
import * as I18nUtils from '~/utils/browser/browser-i18n.utils';
99

1010
describe('i18n.utils.ts', () => {
1111
afterEach(() => {
@@ -16,7 +16,7 @@ describe('i18n.utils.ts', () => {
1616
const chromeI18nMock = vi.fn();
1717
vi.spyOn(I18nStore, 'useI18nStore').mockReturnValue(i18nStoreMock as never);
1818
vi.spyOn(RouterStore, 'useRouterStore').mockReturnValue({ baseUrl: './' } as never);
19-
vi.spyOn(WebExUtils, 'useI18n').mockImplementation(chromeI18nMock);
19+
vi.spyOn(I18nUtils, 'useI18nTranslate').mockImplementation(chromeI18nMock);
2020

2121
it('should use the local store to resolve i18n', () => {
2222
expect.assertions(4);

src/utils/i18n.utils.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import { useI18n as chromeUseI18n } from '@dvcol/web-extension-utils/lib/chrome/utils/i18n.utils';
2-
31
import type { Locale } from '~/models/i18n.model';
42

53
import { useI18nStore } from '~/stores/i18n.store';
64
import { useRouterStore } from '~/stores/router.store';
5+
import { useI18nTranslate } from '~/utils/browser/browser-i18n.utils';
76

87
/**
98
* Setup i18n function to either use chrome i18n resolver or a local store (for web use).
109
* @param roots modules names
1110
* @see chrome.i18n.getMessage
1211
*/
13-
export const useI18n = (...roots: string[]): ReturnType<typeof chromeUseI18n> => {
12+
export const useI18n = (...roots: string[]): ReturnType<typeof useI18nTranslate> => {
1413
if (!window?.chrome?.i18n) {
1514
const store = useI18nStore();
1615
const router = useRouterStore();
@@ -31,5 +30,5 @@ export const useI18n = (...roots: string[]): ReturnType<typeof chromeUseI18n> =>
3130
return (value, ...modules) => store.i18n(value, ...(modules?.length ? modules : roots));
3231
}
3332

34-
return chromeUseI18n(...roots);
33+
return useI18nTranslate(...roots);
3534
};

src/web/init-services.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { traktService } from '~/services/trakt-client/trakt-client.service';
2+
import { useSettingsStore } from '~/stores/settings.store';
3+
4+
export const initServices = async () => {
5+
const { setAuth, syncRestoreAuth } = useSettingsStore();
6+
7+
const auth = await syncRestoreAuth();
8+
9+
if (auth) traktService.importAuthentication(auth).then(() => console.info('TraktClient.importAuthentication', auth));
10+
11+
traktService.onAuthChange(_auth => {
12+
console.info('TraktClient.onAuthChange', _auth);
13+
setAuth(_auth);
14+
});
15+
16+
traktService.onCall(async call => {
17+
console.info('TraktClient.onCall', call);
18+
});
19+
};

src/web/init-vue-app.ts

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { App, Component } from 'vue';
77
import type { RouterOptions } from '~/router';
88

99
import { createRouter } from '~/router';
10+
import { initServices } from '~/web/init-services';
1011

1112
export type InitVueAppOption = RouterOptions;
1213
export const initVueApp = (component: Component, options: InitVueAppOption = {}) => {
@@ -18,6 +19,8 @@ export const initVueApp = (component: Component, options: InitVueAppOption = {})
1819
const router = createRouter(options);
1920
app.use(router);
2021

22+
initServices();
23+
2124
return app;
2225
};
2326

0 commit comments

Comments
 (0)