Skip to content

Commit b18bff6

Browse files
Add a package sdk-vanilla to the repository
Fixes #175 Add a new package `sdk-vanilla` to the repository to provide FusionAuth SDK for vanilla JavaScript with web components. * **Package Metadata and Configuration** - Add `package.json` with package metadata, scripts, dependencies, and build configuration. - Add `vite.config.ts` for Vite build configuration. * **Web Components** - Add `fa-account.ts` to implement the `fa-account` web component with a button to redirect to the user's account management page. - Add `fa-login.ts` to implement the `fa-login` web component with a button to redirect to the /app/login endpoint and start the OAuth flow. - Add `fa-logout.ts` to implement the `fa-logout` web component with a button to redirect to the /app/logout endpoint. - Add `fa-register.ts` to implement the `fa-register` web component with a button to redirect to the /app/register endpoint. * **FusionAuthService** - Add `FusionAuthService.ts` to implement the `FusionAuthService` class with methods for login, register, logout, manage account, fetch user info, refresh token, auto-refresh, and post-redirect handling. - Store and retrieve configuration from localStorage. * **Documentation** - Add `README.md` with instructions and examples on how to use the `sdk-vanilla` package. * **Tests** - Add tests for `FusionAuthService` in `FusionAuthService.test.ts`. - Add tests for web components in `fa-account.test.ts`, `fa-login.test.ts`, `fa-logout.test.ts`, and `fa-register.test.ts`. --- For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/FusionAuth/fusionauth-javascript-sdk/issues/175?shareId=XXXX-XXXX-XXXX-XXXX).
1 parent 1b5643c commit b18bff6

File tree

14 files changed

+580
-0
lines changed

14 files changed

+580
-0
lines changed

packages/sdk-vanilla/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# FusionAuth SDK for Vanilla JavaScript
2+
3+
This package provides a FusionAuth SDK for vanilla JavaScript with web components. It includes the following web components:
4+
5+
- `fa-account`: A button to redirect to the user's account management page.
6+
- `fa-login`: A button component that will redirect the browser to the /app/login endpoint and start the OAuth flow.
7+
- `fa-logout`: A button that will redirect the browser to the /app/logout endpoint.
8+
- `fa-register`: A button that will redirect the browser to the /app/register endpoint.
9+
10+
## Installation
11+
12+
To install the package, use npm or yarn:
13+
14+
```bash
15+
npm install @fusionauth/sdk-vanilla
16+
```
17+
18+
or
19+
20+
```bash
21+
yarn add @fusionauth/sdk-vanilla
22+
```
23+
24+
## Usage
25+
26+
### Importing the Components
27+
28+
To use the web components in your project, import them as follows:
29+
30+
```javascript
31+
import { FaAccount, FaLogin, FaLogout, FaRegister } from '@fusionauth/sdk-vanilla';
32+
```
33+
34+
### Using the Components
35+
36+
You can use the web components in your HTML as follows:
37+
38+
```html
39+
<fa-account></fa-account>
40+
<fa-login></fa-login>
41+
<fa-logout></fa-logout>
42+
<fa-register></fa-register>
43+
```
44+
45+
### Configuring the FusionAuthService
46+
47+
To configure the `FusionAuthService`, use the `configure` method:
48+
49+
```javascript
50+
import { FusionAuthService } from '@fusionauth/sdk-vanilla';
51+
52+
const fusionAuthService = FusionAuthService.configure({
53+
clientId: 'your-client-id',
54+
redirectUri: 'your-redirect-uri',
55+
serverUrl: 'your-server-url',
56+
});
57+
```
58+
59+
## Examples
60+
61+
Here is an example of how to use the `fa-login` component:
62+
63+
```html
64+
<!DOCTYPE html>
65+
<html lang="en">
66+
<head>
67+
<meta charset="UTF-8">
68+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
69+
<title>FusionAuth SDK Vanilla Example</title>
70+
<script type="module">
71+
import { FaLogin } from '@fusionauth/sdk-vanilla';
72+
73+
// Configure the FusionAuthService
74+
const fusionAuthService = FusionAuthService.configure({
75+
clientId: 'your-client-id',
76+
redirectUri: 'your-redirect-uri',
77+
serverUrl: 'your-server-url',
78+
});
79+
</script>
80+
</head>
81+
<body>
82+
<fa-login></fa-login>
83+
</body>
84+
</html>
85+
```
86+
87+
## License
88+
89+
This package is licensed under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for more information.

packages/sdk-vanilla/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "@fusionauth/sdk-vanilla",
3+
"version": "0.1.0",
4+
"description": "FusionAuth SDK for vanilla JavaScript with web components",
5+
"author": "FusionAuth",
6+
"license": "Apache",
7+
"private": true,
8+
"type": "module",
9+
"main": "./dist/index.js",
10+
"module": "./dist/index.js",
11+
"types": "./dist/index.d.ts",
12+
"files": [
13+
"dist"
14+
],
15+
"scripts": {
16+
"build": "vite build",
17+
"test": "vitest --watch=false",
18+
"test:watch": "vitest",
19+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
20+
},
21+
"dependencies": {
22+
"@fusionauth-sdk/core": "*"
23+
},
24+
"devDependencies": {
25+
"typescript": "^5.2.2",
26+
"vite": "^5.2.0",
27+
"vite-plugin-dts": "^3.8.0",
28+
"vitest": "^1.4.0",
29+
"eslint": "^8.32.0"
30+
}
31+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { FusionAuthService } from './FusionAuthService';
3+
import { SDKConfig } from '@fusionauth-sdk/core';
4+
5+
describe('FusionAuthService', () => {
6+
const config: SDKConfig = {
7+
clientId: 'test-client-id',
8+
redirectUri: 'http://localhost:3000',
9+
serverUrl: 'http://localhost:9011',
10+
};
11+
12+
beforeEach(() => {
13+
localStorage.clear();
14+
});
15+
16+
afterEach(() => {
17+
localStorage.clear();
18+
});
19+
20+
it('should configure the FusionAuthService and store config in localStorage', () => {
21+
const service = FusionAuthService.configure(config);
22+
const storedConfig = localStorage.getItem('fusionauth-config');
23+
expect(storedConfig).toBe(JSON.stringify(config));
24+
expect(service).toBeInstanceOf(FusionAuthService);
25+
});
26+
27+
it('should retrieve the config from localStorage', () => {
28+
localStorage.setItem('fusionauth-config', JSON.stringify(config));
29+
const retrievedConfig = FusionAuthService.getConfig();
30+
expect(retrievedConfig).toEqual(config);
31+
});
32+
33+
it('should throw an error if FusionAuthService is not configured', () => {
34+
const service = new FusionAuthService(config);
35+
localStorage.clear();
36+
expect(() => service.startLogin()).toThrowError('FusionAuthService is not configured.');
37+
expect(() => service.startRegister()).toThrowError('FusionAuthService is not configured.');
38+
expect(() => service.startLogout()).toThrowError('FusionAuthService is not configured.');
39+
expect(() => service.manageAccount()).toThrowError('FusionAuthService is not configured.');
40+
expect(() => service.fetchUserInfo()).toThrowError('FusionAuthService is not configured.');
41+
expect(() => service.refreshToken()).toThrowError('FusionAuthService is not configured.');
42+
expect(() => service.initAutoRefresh()).toThrowError('FusionAuthService is not configured.');
43+
expect(() => service.handlePostRedirect()).toThrowError('FusionAuthService is not configured.');
44+
expect(() => service.isLoggedIn).toThrowError('FusionAuthService is not configured.');
45+
});
46+
47+
it('should start login flow', () => {
48+
const service = FusionAuthService.configure(config);
49+
const startLoginSpy = vi.spyOn(service, 'startLogin');
50+
service.startLogin();
51+
expect(startLoginSpy).toHaveBeenCalled();
52+
});
53+
54+
it('should start register flow', () => {
55+
const service = FusionAuthService.configure(config);
56+
const startRegisterSpy = vi.spyOn(service, 'startRegister');
57+
service.startRegister();
58+
expect(startRegisterSpy).toHaveBeenCalled();
59+
});
60+
61+
it('should start logout flow', () => {
62+
const service = FusionAuthService.configure(config);
63+
const startLogoutSpy = vi.spyOn(service, 'startLogout');
64+
service.startLogout();
65+
expect(startLogoutSpy).toHaveBeenCalled();
66+
});
67+
68+
it('should manage account', () => {
69+
const service = FusionAuthService.configure(config);
70+
const manageAccountSpy = vi.spyOn(service, 'manageAccount');
71+
service.manageAccount();
72+
expect(manageAccountSpy).toHaveBeenCalled();
73+
});
74+
75+
it('should fetch user info', async () => {
76+
const service = FusionAuthService.configure(config);
77+
const fetchUserInfoSpy = vi.spyOn(service, 'fetchUserInfo');
78+
await service.fetchUserInfo();
79+
expect(fetchUserInfoSpy).toHaveBeenCalled();
80+
});
81+
82+
it('should refresh token', async () => {
83+
const service = FusionAuthService.configure(config);
84+
const refreshTokenSpy = vi.spyOn(service, 'refreshToken');
85+
await service.refreshToken();
86+
expect(refreshTokenSpy).toHaveBeenCalled();
87+
});
88+
89+
it('should initialize auto refresh', () => {
90+
const service = FusionAuthService.configure(config);
91+
const initAutoRefreshSpy = vi.spyOn(service, 'initAutoRefresh');
92+
service.initAutoRefresh();
93+
expect(initAutoRefreshSpy).toHaveBeenCalled();
94+
});
95+
96+
it('should handle post redirect', () => {
97+
const service = FusionAuthService.configure(config);
98+
const handlePostRedirectSpy = vi.spyOn(service, 'handlePostRedirect');
99+
service.handlePostRedirect();
100+
expect(handlePostRedirectSpy).toHaveBeenCalled();
101+
});
102+
103+
it('should return isLoggedIn status', () => {
104+
const service = FusionAuthService.configure(config);
105+
const isLoggedInSpy = vi.spyOn(service, 'isLoggedIn', 'get');
106+
const isLoggedIn = service.isLoggedIn;
107+
expect(isLoggedInSpy).toHaveBeenCalled();
108+
expect(isLoggedIn).toBe(false);
109+
});
110+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { SDKCore, SDKConfig, UserInfo } from '@fusionauth-sdk/core';
2+
3+
export class FusionAuthService {
4+
private core: SDKCore;
5+
6+
constructor(config: SDKConfig) {
7+
this.core = new SDKCore(config);
8+
}
9+
10+
startLogin(state?: string): void {
11+
const config = FusionAuthService.getConfig();
12+
if (config) {
13+
this.core = new SDKCore(config);
14+
this.core.startLogin(state);
15+
} else {
16+
throw new Error('FusionAuthService is not configured.');
17+
}
18+
}
19+
20+
startRegister(state?: string): void {
21+
const config = FusionAuthService.getConfig();
22+
if (config) {
23+
this.core = new SDKCore(config);
24+
this.core.startRegister(state);
25+
} else {
26+
throw new Error('FusionAuthService is not configured.');
27+
}
28+
}
29+
30+
startLogout(): void {
31+
const config = FusionAuthService.getConfig();
32+
if (config) {
33+
this.core = new SDKCore(config);
34+
this.core.startLogout();
35+
} else {
36+
throw new Error('FusionAuthService is not configured.');
37+
}
38+
}
39+
40+
manageAccount(): void {
41+
const config = FusionAuthService.getConfig();
42+
if (config) {
43+
this.core = new SDKCore(config);
44+
this.core.manageAccount();
45+
} else {
46+
throw new Error('FusionAuthService is not configured.');
47+
}
48+
}
49+
50+
async fetchUserInfo<T = UserInfo>(): Promise<T> {
51+
const config = FusionAuthService.getConfig();
52+
if (config) {
53+
this.core = new SDKCore(config);
54+
return await this.core.fetchUserInfo<T>();
55+
} else {
56+
throw new Error('FusionAuthService is not configured.');
57+
}
58+
}
59+
60+
async refreshToken(): Promise<Response> {
61+
const config = FusionAuthService.getConfig();
62+
if (config) {
63+
this.core = new SDKCore(config);
64+
return await this.core.refreshToken();
65+
} else {
66+
throw new Error('FusionAuthService is not configured.');
67+
}
68+
}
69+
70+
initAutoRefresh(): NodeJS.Timeout | undefined {
71+
const config = FusionAuthService.getConfig();
72+
if (config) {
73+
this.core = new SDKCore(config);
74+
return this.core.initAutoRefresh();
75+
} else {
76+
throw new Error('FusionAuthService is not configured.');
77+
}
78+
}
79+
80+
handlePostRedirect(callback?: (state?: string) => void): void {
81+
const config = FusionAuthService.getConfig();
82+
if (config) {
83+
this.core = new SDKCore(config);
84+
this.core.handlePostRedirect(callback);
85+
} else {
86+
throw new Error('FusionAuthService is not configured.');
87+
}
88+
}
89+
90+
get isLoggedIn(): boolean {
91+
const config = FusionAuthService.getConfig();
92+
if (config) {
93+
this.core = new SDKCore(config);
94+
return this.core.isLoggedIn;
95+
} else {
96+
throw new Error('FusionAuthService is not configured.');
97+
}
98+
}
99+
100+
static configure(config: SDKConfig): FusionAuthService {
101+
localStorage.setItem('fusionauth-config', JSON.stringify(config));
102+
return new FusionAuthService(config);
103+
}
104+
105+
static getConfig(): SDKConfig | null {
106+
const config = localStorage.getItem('fusionauth-config');
107+
return config ? JSON.parse(config) : null;
108+
}
109+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { FaAccount } from './fa-account';
3+
import { FusionAuthService } from '../FusionAuthService';
4+
5+
describe('FaAccount', () => {
6+
let element: FaAccount;
7+
let manageAccountSpy: jest.SpyInstance;
8+
9+
beforeEach(() => {
10+
element = new FaAccount();
11+
document.body.appendChild(element);
12+
manageAccountSpy = jest.spyOn(FusionAuthService.prototype, 'manageAccount');
13+
});
14+
15+
afterEach(() => {
16+
document.body.removeChild(element);
17+
manageAccountSpy.mockRestore();
18+
});
19+
20+
it('should render the button', () => {
21+
const button = element.querySelector('#fa-account-button');
22+
expect(button).toBeTruthy();
23+
expect(button?.textContent).toBe('Manage Account');
24+
});
25+
26+
it('should call manageAccount on button click', () => {
27+
const button = element.querySelector('#fa-account-button');
28+
button?.dispatchEvent(new Event('click'));
29+
expect(manageAccountSpy).toHaveBeenCalled();
30+
});
31+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { FusionAuthService } from '../FusionAuthService';
2+
3+
export class FaAccount extends HTMLElement {
4+
private fusionAuthService: FusionAuthService;
5+
6+
constructor() {
7+
super();
8+
this.fusionAuthService = new FusionAuthService({
9+
clientId: 'your-client-id',
10+
redirectUri: 'your-redirect-uri',
11+
serverUrl: 'your-server-url',
12+
});
13+
}
14+
15+
connectedCallback() {
16+
this.innerHTML = `<button id="fa-account-button">Manage Account</button>`;
17+
this.querySelector('#fa-account-button')?.addEventListener('click', () => {
18+
this.fusionAuthService.manageAccount();
19+
});
20+
}
21+
}
22+
23+
customElements.define('fa-account', FaAccount);

0 commit comments

Comments
 (0)