@abl-solutions/wifi-connect
Version:
The WiFi Connect React Native SDK enables the use of WiFi WPA2-Enterprise networks on mobile devices that are managed and operated by abl solutions GmbH
420 lines (352 loc) • 13.4 kB
text/typescript
import { EmitterSubscription, NativeEventEmitter } from 'react-native';
import { throwOnInvliadResponse } from './http';
export interface WifiConnectOptions {
/**
* An access token that allows access to the WiFi Connect API for the current user.
*/
accessToken: string;
/**
* Usually, network and WiFi support is limited running on a simulator. Therefore,
* the WiFi configuration can not be stored on a simulated device. In such a case
* `WifiConnect.connectToWifi()` will reject with the error code `E_NOT_SUPPORTED_ON_SIMULATOR`.
* If you don't want to reject with this error but instead resolve successfully, set this
* value to `true`. Defaults to `false`.
*/
ignoreNetworkErrorOnSimulator?: boolean;
/**
* Optionally set a different REST API endpoint that should be used by this library.
*/
wifiApiEndpoint?: string;
/**
* Optional Android specific options.
*/
android?: {
/**
* This value is only used for `connectToWifi` on Android 10 devices.
*
* This time span is used to wait for granting/declining the permission dialog which
* is shown by the Operating System. If the user declines the permission dialog within
* the time span, `connectToWifi` will reject with the corresponding error code.
* If the user grants the permissions or the dialog is still opened, `connectToWifi` will
* resolve after the time span.
*
* This value defaults to 10 seconds if not specified.
*/
timeSpanToWaitForPermissionDialogConfirmationInSeconds?: number;
};
/**
* Specifies whether connection represents a hidden network.
*
* This value defaults to false if not specified.
*/
isHiddenSSID?: boolean;
}
export interface LegalTerms {
/**
* Legal terms content
*/
legalTerms: string;
/**
* Version of legal terms.
*/
version: string;
/**
* Minimum legal terms version which must be accepted to access the API.
*/
minimumVersion: string;
/**
* The date at which the minimal version of legal terms will be enforced.
*/
dateMinLegalTermsActive: string;
}
export interface User {
/**
* The e-mail address of the user.
*/
email: string;
/**
* The preferred locale that should be used to interact with the user.
*/
preferredLocale: string;
}
export interface WifiConnectService {
/**
* Register this device to get direct access to abl's WiFi networks. The device will be configured
* to use this WiFi connection. This operation is an asynchrnous process and might take a few seconds
* to complete.
* @param deviceId A string that uniquely identifies this device.
* @param user Details about the user that owns the device.
* @returns A void promise that resolves if the devices registration was successful and the WiFi
* configuration is stored in the device settings. Rejects if configuring the WiFi network failed
* for any reason.
*/
connectToWifi(deviceId: string, user: User): Promise<void>;
/**
* Gets the latest legal terms that must be accepted by the end-user to use the WiFi network.
* @returns A promise that resolves the legal terms or rejects if loading the legal terms failed.
*/
getLatestLegalTerms(): Promise<LegalTerms>;
/**
* Checks whether the specified user already accepted the legal terms or not.
* @returns `true` if the user already accepted the legal terms, otherwise false.
*/
legalTermsAccepted(): Promise<boolean>;
/**
* Checks the version of legal terms accepted by the user.
* @returns version of the legal terms that was accepted by the user. If the user didn't accepted any legal terms yet - returns 'undefined'
*/
legalTermsAcceptedVersion(): Promise<string>;
/**
* Accept the specified version of legal terms. This method needs to be called before a user
* can register to connect to a WiFi network.
* @param legalTermsVersion The version of the legal terms.
*/
acceptLegalTerms(legalTermsVersion: string): Promise<void>;
/**
* Deletes the WiFi settings from the device and unregisters the device
* from abl servers.
* Basically, it just reverts the changes that were made by `connectToWifi()`.
* @param deviceId A string that uniquely identifies this device.
*/
deleteWifiConfiguration(deviceId: string): Promise<void>;
/**
* Check if the device is configured to connect to abl's WiFi network.
* @returns `true` if the device is configured; otherwise `false`.
*/
isWifiConfigured(): Promise<boolean>;
/**
* Check if device is connected to abl's WiFi network.
* Method should be invoked after connectToWifi() to check connection by received SSID, othervise resolves with `false`.
* @returns `true` if the device is connected; otherwise `false`.
*/
isConnectedToWifi(): Promise<boolean>;
/**
* Registers a callback that will be invoked if the required permissions are
* revoked after `connectToWifi` fullfilled. This callback can be used to handle
* the case if a user rejects the permission dialog after the confirmation time span.
*
* This callback should be unregistered (using `unregisterOnPermissionRejectedListener`)
* after usage to free internal resources.
*
* This callback will only be invoked on Android 10 devices.
* @param callback The callback to invoke.
*/
registerOnPermissionRejectedListener(callback: () => void): void;
/**
* Unregister the callback that was previously registered using
* `registerOnPermissionRejectedListener`.
*/
unregisterOnPermissionRejectedListener(): void;
}
export interface NativeWifiConnect {
connectToWifi(args: ConnectToWifiArgs): Promise<void>;
deleteConfiguration(args: DeleteConfigurationArgs): Promise<void>;
isWifiConfigured(): Promise<boolean>;
isConnectedToWifi(ssid: string): Promise<boolean>;
}
export class WifiConnectServiceImpl implements WifiConnectService {
private readonly options: Required<WifiConnectOptions>;
private legalTerms: LegalTerms | null = null;
private permissionRejectedListener: EmitterSubscription | null = null;
private SSID: string | null = null;
constructor(
private readonly nativeWifiConnect: NativeWifiConnect,
wifiConnectionOptions: WifiConnectOptions
) {
this.throwIfInvalidOptions(wifiConnectionOptions);
this.options = this.mergeWithDefaultOptions(wifiConnectionOptions);
}
public async connectToWifi(deviceId: string, user: User): Promise<void> {
const endpoint = `${this.options.wifiApiEndpoint}/api/v1/devices/${deviceId}`;
var response = await fetch(endpoint, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${this.options.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: user.email,
preferredLocale: user.preferredLocale,
}),
});
const body: CreateDeviceResponse = await response.json();
throwOnInvliadResponse(response.status, body);
this.SSID = body.wifiCredentials.authorizedNetwork.ssid;
const args: ConnectToWifiArgs = {
caCertificate: body.wifiCredentials.authorizedNetwork.caCertificate,
domainSuffix: body.wifiCredentials.authorizedNetwork.wpa2Domain,
ignoreNetworkErrorOnSimulator: this.options.ignoreNetworkErrorOnSimulator,
password: body.wifiCredentials.password,
ssid: body.wifiCredentials.authorizedNetwork.ssid,
username: body.wifiCredentials.username,
timeSpanToWaitForPermissionDialogConfirmationInSeconds:
this.options.android
.timeSpanToWaitForPermissionDialogConfirmationInSeconds ?? 10,
isHiddenSSID: this.options.isHiddenSSID,
};
await this.nativeWifiConnect.connectToWifi(args);
}
public async getLatestLegalTerms(): Promise<LegalTerms> {
if (this.legalTerms) {
return this.legalTerms;
}
const endpoint = `${this.options.wifiApiEndpoint}/api/v1/legal-terms`;
var response = await fetch(endpoint, {
method: 'GET',
headers: {
Authorization: `Bearer ${this.options.accessToken}`,
},
});
if (response.status !== 200) {
throw new Error(
`Could not load legal terms. Received status ${response.status} from API.`
);
}
const body: LegalTerms = await response.json();
this.legalTerms = body;
return body;
}
public async legalTermsAccepted(): Promise<boolean> {
const legalTerms = await this.getLatestLegalTerms();
const endpoint = `${this.options.wifiApiEndpoint}/api/v1/legal-terms/acceptedVersion`;
var response = await fetch(endpoint, {
method: 'GET',
headers: {
Authorization: `Bearer ${this.options.accessToken}`,
},
});
// The API will return a 404 if the current user did not accept any legal terms version at all.
if (response.status === 404) {
return false;
}
if (response.status !== 200) {
throw new Error(
`Could not load the currently accepted version. Received status ${response.status} from API.`
);
}
const body: AcceptedLegalTermsResponse = await response.json();
return body.version === legalTerms.version;
}
public async legalTermsAcceptedVersion(): Promise<string> {
const endpoint = `${this.options.wifiApiEndpoint}/api/v1/legal-terms/acceptedVersion`;
var response = await fetch(endpoint, {
method: 'GET',
headers: {
Authorization: `Bearer ${this.options.accessToken}`,
},
});
// The API will return a 404 if the current user did not accept any legal terms version at all.
if (response.status === 404) {
return 'undefined';
}
if (response.status !== 200) {
throw new Error(
`Could not load the currently accepted version. Received status ${response.status} from API.`
);
}
const body: AcceptedLegalTermsResponse = await response.json();
return body.version;
}
public async acceptLegalTerms(legalTermsVersion: string): Promise<void> {
const endpoint = `${this.options.wifiApiEndpoint}/api/v1/legal-terms/${legalTermsVersion}/accept`;
var response = await fetch(endpoint, {
method: 'PUT',
headers: {
Authorization: `Bearer ${this.options.accessToken}`,
},
});
if (response.status !== 200 && response.status !== 202) {
throw new Error(
`Accepting the legal terms failed. Received status ${response.status} from API.`
);
}
}
public async deleteWifiConfiguration(deviceId: string): Promise<void> {
const endpoint = `${this.options.wifiApiEndpoint}/api/v1/devices/${deviceId}`;
var response = await fetch(endpoint, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${this.options.accessToken}`,
},
});
throwOnInvliadResponse(response.status, null, true);
const args: DeleteConfigurationArgs = {
ignoreNetworkErrorOnSimulator: this.options.ignoreNetworkErrorOnSimulator,
};
await this.nativeWifiConnect.deleteConfiguration(args);
}
public async isWifiConfigured(): Promise<boolean> {
return await this.nativeWifiConnect.isWifiConfigured();
}
public registerOnPermissionRejectedListener(callback: () => void): void {
const eventEmitter = new NativeEventEmitter(this.nativeWifiConnect as any);
this.permissionRejectedListener = eventEmitter.addListener(
'PermissionRejected',
() => callback()
);
}
public unregisterOnPermissionRejectedListener(): void {
const listener = this.permissionRejectedListener;
if (listener) {
listener.remove();
this.permissionRejectedListener = null;
}
}
public isConnectedToWifi = (): Promise<boolean> =>
this.SSID
? this.nativeWifiConnect.isConnectedToWifi(this.SSID)
: Promise.resolve(false);
private mergeWithDefaultOptions(
options: WifiConnectOptions
): Required<WifiConnectOptions> {
return {
accessToken: options.accessToken,
ignoreNetworkErrorOnSimulator:
options.ignoreNetworkErrorOnSimulator ?? false,
wifiApiEndpoint:
options?.wifiApiEndpoint ||
'https://api.wifi.connectivity.abl-solutions.io',
android: options.android ?? {},
isHiddenSSID: options?.isHiddenSSID || false,
};
}
private throwIfInvalidOptions(options: WifiConnectOptions) {
if (!options) {
throw new Error('WifiConnectOptions must be not null.');
}
if (!options.accessToken) {
throw new Error('WifiConnectOptions.accessToken must be not null.');
}
if (options.accessToken === '') {
throw new Error('WifiConnectOptions.accessToken must be not empty.');
}
}
}
interface AcceptedLegalTermsResponse {
version: string;
}
interface CreateDeviceResponse {
deviceId: string;
wifiCredentials: {
username: string;
password: string;
authorizedNetwork: {
caCertificate: string;
ssid: string;
wpa2Domain: string;
};
};
}
interface ConnectToWifiArgs extends CommonNativeArgs {
caCertificate: string;
domainSuffix: string;
password: string;
ssid: string;
username: string;
timeSpanToWaitForPermissionDialogConfirmationInSeconds: number;
isHiddenSSID: boolean;
}
interface DeleteConfigurationArgs extends CommonNativeArgs {}
interface CommonNativeArgs {
ignoreNetworkErrorOnSimulator: boolean;
}