mirror of https://github.com/OpenVidu/openvidu.git
ov-components: Refactor videoconference component to use centralized state management
parent
8af9f75a10
commit
04b8b741e2
|
@ -1,16 +1,16 @@
|
||||||
<div id="call-container">
|
<div id="call-container">
|
||||||
<div id="spinner" *ngIf="loading">
|
<div id="spinner" *ngIf="componentState.isLoading">
|
||||||
<mat-spinner [diameter]="50"></mat-spinner>
|
<mat-spinner [diameter]="spinnerDiameter"></mat-spinner>
|
||||||
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
|
<span>{{ 'PREJOIN.PREPARING' | translate }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div [@inOutAnimation] id="pre-join-container" *ngIf="showPrejoin && !loading">
|
<div [@inOutAnimation] id="pre-join-container" *ngIf="componentState.showPrejoin && !componentState.isLoading">
|
||||||
<ng-container *ngIf="openviduAngularPreJoinTemplate; else defaultPreJoin">
|
<ng-container *ngIf="openviduAngularPreJoinTemplate; else defaultPreJoin">
|
||||||
<ng-container *ngTemplateOutlet="openviduAngularPreJoinTemplate"></ng-container>
|
<ng-container *ngTemplateOutlet="openviduAngularPreJoinTemplate"></ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #defaultPreJoin>
|
<ng-template #defaultPreJoin>
|
||||||
<ov-pre-join
|
<ov-pre-join
|
||||||
[error]="_tokenError"
|
[error]="componentState.error?.tokenError"
|
||||||
(onReadyToJoin)="_onReadyToJoin()"
|
(onReadyToJoin)="_onReadyToJoin()"
|
||||||
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
(onVideoDeviceChanged)="onVideoDeviceChanged.emit($event)"
|
||||||
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
|
(onVideoEnabledChanged)="onVideoEnabledChanged.emit($event)"
|
||||||
|
@ -21,12 +21,12 @@
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="spinner" *ngIf="!loading && error">
|
<div id="spinner" *ngIf="!componentState.isLoading && componentState.error?.hasError">
|
||||||
<mat-icon class="error-icon">error</mat-icon>
|
<mat-icon class="error-icon">error</mat-icon>
|
||||||
<span>{{ errorMessage }}</span>
|
<span>{{ componentState.error?.message }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div [@inOutAnimation] id="vc-container" *ngIf="isRoomReady && !showPrejoin && !loading && !error">
|
<div [@inOutAnimation] id="vc-container" *ngIf="componentState.isRoomReady && !componentState.showPrejoin && !componentState.isLoading && !componentState.error?.hasError">
|
||||||
<ov-session
|
<ov-session
|
||||||
(onRoomCreated)="onRoomCreated.emit($event)"
|
(onRoomCreated)="onRoomCreated.emit($event)"
|
||||||
(onRoomReconnecting)="onRoomDisconnected.emit()"
|
(onRoomReconnecting)="onRoomDisconnected.emit()"
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
ToolbarDirective
|
ToolbarDirective
|
||||||
} from '../../directives/template/openvidu-components-angular.directive';
|
} from '../../directives/template/openvidu-components-angular.directive';
|
||||||
import { ILogger } from '../../models/logger.model';
|
import { ILogger } from '../../models/logger.model';
|
||||||
|
import { VideoconferenceState, VideoconferenceStateInfo } from '../../models/videoconference-state.model';
|
||||||
import { ActionService } from '../../services/action/action.service';
|
import { ActionService } from '../../services/action/action.service';
|
||||||
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
|
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
|
||||||
import { DeviceService } from '../../services/device/device.service';
|
import { DeviceService } from '../../services/device/device.service';
|
||||||
|
@ -52,13 +53,20 @@ import { LangOption } from '../../models/lang.model';
|
||||||
styleUrls: ['./videoconference.component.scss'],
|
styleUrls: ['./videoconference.component.scss'],
|
||||||
animations: [
|
animations: [
|
||||||
trigger('inOutAnimation', [
|
trigger('inOutAnimation', [
|
||||||
transition(':enter', [style({ opacity: 0 }), animate('300ms ease-out', style({ opacity: 1 }))])
|
transition(':enter', [style({ opacity: 0 }), animate(`${VideoconferenceComponent.ANIMATION_DURATION_MS}ms ease-out`, style({ opacity: 1 }))])
|
||||||
// transition(':leave', [style({ opacity: 1 }), animate('50ms ease-in', style({ opacity: 0.9 }))])
|
// transition(':leave', [style({ opacity: 1 }), animate('50ms ease-in', style({ opacity: 0.9 }))])
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
private static readonly PARTICIPANT_NAME_TIMEOUT_MS = 1000;
|
||||||
|
private static readonly ANIMATION_DURATION_MS = 300;
|
||||||
|
private static readonly MATERIAL_ICONS_URL = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&icon_names=background_replace,keep_off';
|
||||||
|
private static readonly MATERIAL_ICONS_SELECTOR = 'link[href*="Material+Symbols+Outlined"]';
|
||||||
|
private static readonly SPINNER_DIAMETER = 50;
|
||||||
// *** Toolbar ***
|
// *** Toolbar ***
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
@ -379,41 +387,55 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
* Centralized state management for the videoconference component
|
||||||
*/
|
*/
|
||||||
error: boolean = false;
|
componentState: VideoconferenceStateInfo = {
|
||||||
/**
|
state: VideoconferenceState.INITIALIZING,
|
||||||
* @internal
|
showPrejoin: true,
|
||||||
*/
|
isRoomReady: false,
|
||||||
errorMessage: string = '';
|
isConnected: false,
|
||||||
/**
|
hasAudioDevices: false,
|
||||||
* @internal
|
hasVideoDevices: false,
|
||||||
*/
|
hasUserInitiatedJoin: false,
|
||||||
showPrejoin: boolean = true;
|
wasPrejoinShown: false,
|
||||||
|
isLoading: true,
|
||||||
/**
|
error: {
|
||||||
* @internal
|
hasError: false,
|
||||||
* Tracks if user has initiated the join process to prevent prejoin from showing again
|
message: '',
|
||||||
*/
|
tokenError: null
|
||||||
private hasUserInitiatedJoin: boolean = false;
|
}
|
||||||
|
};
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
isRoomReady: boolean = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
loading = true;
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
_tokenError: any;
|
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private log: ILogger;
|
private log: ILogger;
|
||||||
private latestParticipantName: string | undefined;
|
private latestParticipantName: string | undefined;
|
||||||
|
|
||||||
|
// Expose constants to template
|
||||||
|
get spinnerDiameter(): number {
|
||||||
|
return VideoconferenceComponent.SPINNER_DIAMETER;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Updates the component state
|
||||||
|
*/
|
||||||
|
private updateComponentState(newState: Partial<VideoconferenceStateInfo>): void {
|
||||||
|
this.componentState = { ...this.componentState, ...newState };
|
||||||
|
this.log.d(`State updated to: ${this.componentState.state}`, this.componentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* Checks if user has initiated the join process
|
||||||
|
*/
|
||||||
|
private hasUserInitiatedJoin(): boolean {
|
||||||
|
return (
|
||||||
|
this.componentState.state === VideoconferenceState.JOINING ||
|
||||||
|
this.componentState.state === VideoconferenceState.READY_TO_CONNECT ||
|
||||||
|
this.componentState.state === VideoconferenceState.CONNECTED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
|
@ -426,6 +448,17 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
private libService: OpenViduComponentsConfigService
|
private libService: OpenViduComponentsConfigService
|
||||||
) {
|
) {
|
||||||
this.log = this.loggerSrv.get('VideoconferenceComponent');
|
this.log = this.loggerSrv.get('VideoconferenceComponent');
|
||||||
|
|
||||||
|
// Initialize state
|
||||||
|
this.updateComponentState({
|
||||||
|
state: VideoconferenceState.INITIALIZING,
|
||||||
|
showPrejoin: true,
|
||||||
|
isRoomReady: false,
|
||||||
|
wasPrejoinShown: false,
|
||||||
|
isLoading: true,
|
||||||
|
error: { hasError: false }
|
||||||
|
});
|
||||||
|
|
||||||
this.subscribeToVideconferenceDirectives();
|
this.subscribeToVideconferenceDirectives();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,7 +474,11 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.addMaterialIconsIfNeeded();
|
this.addMaterialIconsIfNeeded();
|
||||||
this.setupTemplates();
|
this.setupTemplates();
|
||||||
this.deviceSrv.initializeDevices().then(() => (this.loading = false));
|
this.deviceSrv.initializeDevices().then(() => {
|
||||||
|
this.updateComponentState({
|
||||||
|
isLoading: false
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -449,10 +486,10 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
*/
|
*/
|
||||||
private addMaterialIconsIfNeeded(): void {
|
private addMaterialIconsIfNeeded(): void {
|
||||||
//Add material icons to the page if not already present
|
//Add material icons to the page if not already present
|
||||||
const existingLink = document.querySelector('link[href*="Material+Symbols+Outlined"]');
|
const existingLink = document.querySelector(VideoconferenceComponent.MATERIAL_ICONS_SELECTOR);
|
||||||
if (!existingLink) {
|
if (!existingLink) {
|
||||||
const link = document.createElement('link');
|
const link = document.createElement('link');
|
||||||
link.href = 'https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined&icon_names=background_replace,keep_off';
|
link.href = VideoconferenceComponent.MATERIAL_ICONS_URL;
|
||||||
link.rel = 'stylesheet';
|
link.rel = 'stylesheet';
|
||||||
document.head.appendChild(link);
|
document.head.appendChild(link);
|
||||||
}
|
}
|
||||||
|
@ -630,17 +667,23 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Mark that user has initiated the join process
|
// Mark that user has initiated the join process
|
||||||
this.hasUserInitiatedJoin = true;
|
this.updateComponentState({
|
||||||
|
state: VideoconferenceState.JOINING,
|
||||||
|
wasPrejoinShown: this.componentState.showPrejoin
|
||||||
|
});
|
||||||
|
|
||||||
// Always initialize the room when ready to join
|
// Always initialize the room when ready to join
|
||||||
this.openviduService.initRoom();
|
this.openviduService.initRoom();
|
||||||
|
|
||||||
const participantName = this.latestParticipantName;
|
const participantName = this.latestParticipantName;
|
||||||
|
|
||||||
if (this.isRoomReady) {
|
if (this.componentState.isRoomReady) {
|
||||||
// Room is ready, hide prejoin and proceed
|
// Room is ready, hide prejoin and proceed
|
||||||
this.log.d('Room is ready, proceeding to join');
|
this.log.d('Room is ready, proceeding to join');
|
||||||
this.showPrejoin = false;
|
this.updateComponentState({
|
||||||
|
state: VideoconferenceState.READY_TO_CONNECT,
|
||||||
|
showPrejoin: false
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Room not ready, request token if we have a participant name
|
// Room not ready, request token if we have a participant name
|
||||||
if (participantName) {
|
if (participantName) {
|
||||||
|
@ -651,26 +694,34 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasPrejoinShown = this.showPrejoin;
|
|
||||||
|
|
||||||
// Emit onReadyToJoin event only if prejoin page was actually shown
|
// Emit onReadyToJoin event only if prejoin page was actually shown
|
||||||
// This ensures the event semantics are correct
|
// This ensures the event semantics are correct
|
||||||
if (wasPrejoinShown) {
|
if (this.componentState.wasPrejoinShown) {
|
||||||
this.log.d('Emitting onReadyToJoin event (prejoin was shown)');
|
this.log.d('Emitting onReadyToJoin event (prejoin was shown)');
|
||||||
this.onReadyToJoin.emit();
|
this.onReadyToJoin.emit();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.e('Error during ready to join process', error);
|
this.log.e('Error during ready to join process', error);
|
||||||
// Could emit an error event or handle gracefully based on requirements
|
this.updateComponentState({
|
||||||
|
state: VideoconferenceState.ERROR,
|
||||||
|
error: {
|
||||||
|
hasError: true,
|
||||||
|
message: 'Error during ready to join process'
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
_onParticipantLeft(event: ParticipantLeftEvent) {
|
_onParticipantLeft(event: ParticipantLeftEvent) {
|
||||||
this.isRoomReady = false;
|
// Reset to disconnected state to allow prejoin to show again if needed
|
||||||
// Reset join initiation flag to allow prejoin to show again if needed
|
this.updateComponentState({
|
||||||
this.hasUserInitiatedJoin = false;
|
state: VideoconferenceState.DISCONNECTED,
|
||||||
|
isRoomReady: false,
|
||||||
|
showPrejoin: this.libService.showPrejoin()
|
||||||
|
});
|
||||||
|
|
||||||
this.onParticipantLeft.emit(event);
|
this.onParticipantLeft.emit(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -685,20 +736,34 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
const livekitUrl = this.libService.getLivekitUrl();
|
const livekitUrl = this.libService.getLivekitUrl();
|
||||||
this.openviduService.initializeAndSetToken(token, livekitUrl);
|
this.openviduService.initializeAndSetToken(token, livekitUrl);
|
||||||
this.log.d('Token has been successfully set. Room is ready to join');
|
this.log.d('Token has been successfully set. Room is ready to join');
|
||||||
this.isRoomReady = true;
|
|
||||||
|
|
||||||
// Only update showPrejoin if user hasn't initiated join process yet
|
// Only update showPrejoin if user hasn't initiated join process yet
|
||||||
// This prevents prejoin from showing again after user clicked join
|
// This prevents prejoin from showing again after user clicked join
|
||||||
if (!this.hasUserInitiatedJoin) {
|
if (!this.hasUserInitiatedJoin()) {
|
||||||
this.showPrejoin = this.libService.showPrejoin();
|
this.updateComponentState({
|
||||||
|
state: VideoconferenceState.PREJOIN_SHOWN,
|
||||||
|
isRoomReady: true,
|
||||||
|
showPrejoin: this.libService.showPrejoin()
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// User has initiated join, proceed to hide prejoin and continue
|
// User has initiated join, proceed to hide prejoin and continue
|
||||||
this.log.d('User has initiated join, hiding prejoin and proceeding');
|
this.log.d('User has initiated join, hiding prejoin and proceeding');
|
||||||
this.showPrejoin = false;
|
this.updateComponentState({
|
||||||
|
state: VideoconferenceState.READY_TO_CONNECT,
|
||||||
|
isRoomReady: true,
|
||||||
|
showPrejoin: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.e('Error trying to set token', error);
|
this.log.e('Error trying to set token', error);
|
||||||
this._tokenError = error;
|
this.updateComponentState({
|
||||||
|
state: VideoconferenceState.ERROR,
|
||||||
|
error: {
|
||||||
|
hasError: true,
|
||||||
|
message: 'Error setting token',
|
||||||
|
tokenError: error
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -706,16 +771,26 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
if (!error) return;
|
if (!error) return;
|
||||||
|
|
||||||
this.log.e('Token error received', error);
|
this.log.e('Token error received', error);
|
||||||
this._tokenError = error;
|
this.updateComponentState({
|
||||||
|
state: VideoconferenceState.ERROR,
|
||||||
|
error: {
|
||||||
|
hasError: true,
|
||||||
|
message: 'Token error',
|
||||||
|
tokenError: error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!this.showPrejoin) {
|
if (!this.componentState.showPrejoin) {
|
||||||
this.actionService.openDialog(error.name, error.message, false);
|
this.actionService.openDialog(error.name, error.message, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.libService.prejoin$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
this.libService.prejoin$.pipe(takeUntil(this.destroy$)).subscribe((value: boolean) => {
|
||||||
this.showPrejoin = value;
|
this.updateComponentState({
|
||||||
if (!this.showPrejoin) {
|
showPrejoin: value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
// Emit token ready if the prejoin page won't be shown
|
// Emit token ready if the prejoin page won't be shown
|
||||||
|
|
||||||
// Ensure we have a participant name before proceeding with the join
|
// Ensure we have a participant name before proceeding with the join
|
||||||
|
@ -747,10 +822,9 @@ export class VideoconferenceComponent implements OnDestroy, AfterViewInit {
|
||||||
}
|
}
|
||||||
this._onReadyToJoin();
|
this._onReadyToJoin();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, VideoconferenceComponent.PARTICIPANT_NAME_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// this.cd.markForCheck();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((name: string) => {
|
this.libService.participantName$.pipe(takeUntil(this.destroy$)).subscribe((name: string) => {
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* Enum representing the possible states of the videoconference component
|
||||||
|
*/
|
||||||
|
export enum VideoconferenceState {
|
||||||
|
/**
|
||||||
|
* Initial state when the component is loading
|
||||||
|
*/
|
||||||
|
INITIALIZING = 'INITIALIZING',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prejoin page is being shown to the user
|
||||||
|
*/
|
||||||
|
PREJOIN_SHOWN = 'PREJOIN_SHOWN',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has initiated the join process, waiting for token
|
||||||
|
*/
|
||||||
|
JOINING = 'JOINING',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token received and room is ready to connect
|
||||||
|
*/
|
||||||
|
READY_TO_CONNECT = 'READY_TO_CONNECT',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successfully connected to the room
|
||||||
|
*/
|
||||||
|
CONNECTED = 'CONNECTED',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnected from the room
|
||||||
|
*/
|
||||||
|
DISCONNECTED = 'DISCONNECTED',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error state
|
||||||
|
*/
|
||||||
|
ERROR = 'ERROR'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface representing the state information of the videoconference component
|
||||||
|
*/
|
||||||
|
export interface VideoconferenceStateInfo {
|
||||||
|
/**
|
||||||
|
* Current state of the videoconference
|
||||||
|
*/
|
||||||
|
state: VideoconferenceState;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether prejoin page should be visible
|
||||||
|
*/
|
||||||
|
showPrejoin: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether room is ready for connection
|
||||||
|
*/
|
||||||
|
isRoomReady: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether user is connected to the room
|
||||||
|
*/
|
||||||
|
isConnected: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether audio devices are available
|
||||||
|
*/
|
||||||
|
hasAudioDevices: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether video devices are available
|
||||||
|
*/
|
||||||
|
hasVideoDevices: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether user has initiated the join process
|
||||||
|
*/
|
||||||
|
hasUserInitiatedJoin: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether prejoin was shown to the user at least once
|
||||||
|
*/
|
||||||
|
wasPrejoinShown: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the component is in loading state
|
||||||
|
*/
|
||||||
|
isLoading: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error information if any
|
||||||
|
*/
|
||||||
|
error?: {
|
||||||
|
hasError: boolean;
|
||||||
|
message?: string;
|
||||||
|
tokenError?: any;
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue