Skip to content

Commit

Permalink
Fix Google Login button disappearing issue (#3197)
Browse files Browse the repository at this point in the history
### Purpose:
The current Google login button occasionally disappears. The reason is
that the Google script is loaded statically, meaning it is automatically
loaded via the `<script>` tag when the page loads. This approach has a
potential issue: if we want to use the `onGoogleLibraryLoad` callback
function to confirm that the script has successfully loaded, we must
ensure that the callback is defined before the script finishes loading.
Otherwise, when the script completes loading, Google will check whether
`onGoogleLibraryLoad` is defined. If it is not defined, the script load
notification will be missed, and the Google login initialization logic
will not execute.
fix #3155

### Changes:
1. To reduce the maintenance cost caused by frequent updates to the
Google official API, we chose to implement Google login using the
third-party library angularx-social-login instead of directly relying on
the Google official API.
2. Added the dependency @abacritt/angularx-social-login (version 2.1.0).
Please use `yarn install` to install it.
3. Additionally, a test file for the Google Login component has been
added to verify whether the component initializes correctly after the
script is successfully loaded.

### Demos:
Before:


https://github.com/user-attachments/assets/2717e6cb-d250-49e0-a09d-d3d11ffc7be3

After:


https://github.com/user-attachments/assets/81d9e48b-02ee-48ad-8481-d7a50ce43ba0

---------

Co-authored-by: Chris <[email protected]>
Co-authored-by: Xinyuan Lin <[email protected]>
  • Loading branch information
3 people authored Jan 19, 2025
1 parent c5c0701 commit 58a214f
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 88 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ class GoogleAuthResource {

@GET
@Path("/clientid")
def getClientId: String = {
clientId
}
def getClientId: String = clientId

@POST
@Consumes(Array(MediaType.TEXT_PLAIN))
Expand Down
1 change: 1 addition & 0 deletions core/gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"private": true,
"dependencies": {
"@abacritt/angularx-social-login": "2.1.0",
"@ali-hm/angular-tree-component": "12.0.5",
"@angular/animations": "16.2.12",
"@angular/cdk": "16.2.12",
Expand Down
22 changes: 20 additions & 2 deletions core/gui/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,16 @@ import { SearchBarComponent } from "./dashboard/component/user/search-bar/search
import { ListItemComponent } from "./dashboard/component/user/list-item/list-item.component";
import { HubComponent } from "./hub/component/hub.component";
import { HubWorkflowSearchComponent } from "./hub/component/workflow/search/hub-workflow-search.component";
import { GoogleLoginComponent } from "./dashboard/component/user/google-login/google-login.component";
import { HubWorkflowComponent } from "./hub/component/workflow/hub-workflow.component";
import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component";
import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component";
import { BrowseSectionComponent } from "./hub/component/browse-section/browse-section.component";
import { BreakpointConditionInputComponent } from "./workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component";
import { CodeDebuggerComponent } from "./workspace/component/code-editor-dialog/code-debugger.component";
import { GoogleAuthService } from "./common/service/user/google-auth.service";
import { SocialLoginModule, SocialAuthServiceConfig, GoogleSigninButtonModule } from "@abacritt/angularx-social-login";
import { GoogleLoginProvider } from "@abacritt/angularx-social-login";
import { lastValueFrom } from "rxjs";

registerLocaleData(en);

Expand Down Expand Up @@ -225,7 +228,6 @@ registerLocaleData(en);
HubWorkflowComponent,
HubWorkflowSearchComponent,
HubWorkflowDetailComponent,
GoogleLoginComponent,
LandingPageComponent,
BrowseSectionComponent,
BreakpointConditionInputComponent,
Expand Down Expand Up @@ -289,6 +291,8 @@ registerLocaleData(en);
NzTreeViewModule,
NzNoAnimationModule,
TreeModule,
SocialLoginModule,
GoogleSigninButtonModule,
],
providers: [
provideNzI18n(en_US),
Expand All @@ -303,6 +307,20 @@ registerLocaleData(en);
useClass: BlobErrorHttpInterceptor,
multi: true,
},
{
provide: "SocialAuthServiceConfig",
useFactory: (googleAuthService: GoogleAuthService, userService: UserService) => {
return lastValueFrom(googleAuthService.getClientId()).then(clientId => ({
providers: [
{
id: GoogleLoginProvider.PROVIDER_ID,
provider: new GoogleLoginProvider(clientId, { oneTapEnabled: !userService.isLogin() }),
},
],
})) as Promise<SocialAuthServiceConfig>;
},
deps: [GoogleAuthService, UserService],
},
],
bootstrap: [AppComponent],
})
Expand Down
8 changes: 7 additions & 1 deletion core/gui/src/app/common/service/user/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ export class AuthService {
public googleAuth(credential: string): Observable<Readonly<{ accessToken: string }>> {
return this.http.post<Readonly<{ accessToken: string }>>(
`${AppSettings.getApiEndpoint()}/${AuthService.GOOGLE_LOGIN_ENDPOINT}`,
`${credential}`
credential,
{
headers: {
"Content-Type": "text/plain",
Accept: "application/json",
},
}
);
}

Expand Down
33 changes: 4 additions & 29 deletions core/gui/src/app/common/service/user/google-auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,15 @@
import { Injectable } from "@angular/core";
import { Subject } from "rxjs";
import { Observable } from "rxjs";
import { HttpClient } from "@angular/common/http";
import { AppSettings } from "../../app-setting";
declare var window: any;
export interface CredentialResponse {
client_id: string;
credential: string;
select_by: string;
}

@Injectable({
providedIn: "root",
})
export class GoogleAuthService {
private _googleCredentialResponse = new Subject<CredentialResponse>();
constructor(private http: HttpClient) {}
public googleAuthInit(parent: HTMLElement | null) {
this.http.get(`${AppSettings.getApiEndpoint()}/auth/google/clientid`, { responseType: "text" }).subscribe({
next: response => {
window.onGoogleLibraryLoad = () => {
window.google.accounts.id.initialize({
client_id: response,
callback: (auth: CredentialResponse) => {
this._googleCredentialResponse.next(auth);
},
});
window.google.accounts.id.renderButton(parent, { width: 200 });
window.google.accounts.id.prompt();
};
},
error: (err: unknown) => {
console.error(err);
},
});
}

get googleCredentialResponse() {
return this._googleCredentialResponse.asObservable();
getClientId(): Observable<string> {
return this.http.get(`${AppSettings.getApiEndpoint()}/auth/google/clientid`, { responseType: "text" });
}
}
8 changes: 6 additions & 2 deletions core/gui/src/app/dashboard/component/dashboard.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,17 @@
<div class="page-container">
<nz-layout>
<div
*ngIf="displayNavbar"
[hidden]="!displayNavbar"
id="nav">
<texera-search-bar></texera-search-bar>
<ng-container *ngIf="isLogin">
<texera-user-icon></texera-user-icon>
</ng-container>
<texera-google-login [hidden]="isLogin"></texera-google-login>
<asl-google-signin-button
*ngIf="!isLogin"
type="standard"
size="large"
width="200"></asl-google-signin-button>
</div>

<nz-content>
Expand Down
11 changes: 6 additions & 5 deletions core/gui/src/app/dashboard/component/dashboard.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ texera-search-bar {
padding-right: 10px;
}

texera-google-login {
float: right;
padding: 5px 0;
}

texera-user-icon {
padding: 0 24px;
}
Expand Down Expand Up @@ -70,3 +65,9 @@ nz-content {
.hidden {
display: none;
}

#nav {
max-width: 100%;
max-height: 100%;
overflow: hidden;
}
125 changes: 125 additions & 0 deletions core/gui/src/app/dashboard/component/dashboard.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { TestBed, ComponentFixture } from "@angular/core/testing";
import { DashboardComponent } from "./dashboard.component";
import { NO_ERRORS_SCHEMA, ChangeDetectorRef, NgZone, EventEmitter } from "@angular/core";
import { By } from "@angular/platform-browser";
import { of } from "rxjs";

import { UserService } from "../../common/service/user/user.service";
import { FlarumService } from "../service/user/flarum/flarum.service";
import { SocialAuthService } from "@abacritt/angularx-social-login";
import {
Router,
NavigationEnd,
ActivatedRoute,
ActivatedRouteSnapshot,
UrlSegment,
Params,
Data,
} from "@angular/router";
import { convertToParamMap } from "@angular/router";

describe("DashboardComponent", () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;

let userServiceMock: Partial<UserService>;
let routerMock: Partial<Router>;
let flarumServiceMock: Partial<FlarumService>;
let cdrMock: Partial<ChangeDetectorRef>;
let ngZoneMock: Partial<NgZone>;
let socialAuthServiceMock: Partial<SocialAuthService>;
let activatedRouteMock: Partial<ActivatedRoute>;

const activatedRouteSnapshotMock: Partial<ActivatedRouteSnapshot> = {
queryParams: {},
url: [] as UrlSegment[],
params: {} as Params,
fragment: null,
data: {} as Data,
paramMap: convertToParamMap({}),
queryParamMap: convertToParamMap({}),
outlet: "",
routeConfig: null,
root: null as any,
parent: null as any,
firstChild: null as any,
children: [],
pathFromRoot: [],
};

beforeEach(async () => {
userServiceMock = {
isAdmin: jasmine.createSpy("isAdmin").and.returnValue(false),
isLogin: jasmine.createSpy("isLogin").and.returnValue(false),
userChanged: jasmine.createSpy("userChanged").and.returnValue(of(null)),
};

routerMock = {
events: of(new NavigationEnd(1, "/dashboard", "/dashboard")),
url: "/dashboard",
navigateByUrl: jasmine.createSpy("navigateByUrl"),
};

flarumServiceMock = {
auth: jasmine.createSpy("auth").and.returnValue(of({ token: "dummyToken" })),
register: jasmine.createSpy("register").and.returnValue(of(null)),
};

cdrMock = {
detectChanges: jasmine.createSpy("detectChanges"),
};

ngZoneMock = {
hasPendingMicrotasks: false,
hasPendingMacrotasks: false,
onUnstable: new EventEmitter<any>(),
onMicrotaskEmpty: new EventEmitter<any>(),
onStable: new EventEmitter<any>(),
onError: new EventEmitter<any>(),
run: (fn: () => any) => fn(),
runGuarded: (fn: () => any) => fn(),
runOutsideAngular: (fn: () => any) => fn(),
runTask: (fn: () => any) => fn(),
};

socialAuthServiceMock = {
authState: of(),
};

activatedRouteMock = {
snapshot: activatedRouteSnapshotMock as ActivatedRouteSnapshot,
};

await TestBed.configureTestingModule({
declarations: [DashboardComponent],
providers: [
{ provide: UserService, useValue: userServiceMock },
{ provide: Router, useValue: routerMock },
{ provide: FlarumService, useValue: flarumServiceMock },
{ provide: ChangeDetectorRef, useValue: cdrMock },
{ provide: NgZone, useValue: ngZoneMock },
{ provide: SocialAuthService, useValue: socialAuthServiceMock },
{ provide: ActivatedRoute, useValue: activatedRouteMock },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it("should create the component", () => {
expect(component).toBeTruthy();
});

it("should render Google sign-in button when user is NOT logged in", () => {
(userServiceMock.isLogin as jasmine.Spy).and.returnValue(false);
fixture.detectChanges();

const googleSignInBtn = fixture.debugElement.query(By.css("asl-google-signin-button"));
expect(googleSignInBtn).toBeTruthy();
});
});
23 changes: 17 additions & 6 deletions core/gui/src/app/dashboard/component/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { UserService } from "../../common/service/user/user.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { FlarumService } from "../service/user/flarum/flarum.service";
import { HttpErrorResponse } from "@angular/common/http";
import { NavigationEnd, Router } from "@angular/router";
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
import { HubComponent } from "../../hub/component/hub.component";
import { SocialAuthService } from "@abacritt/angularx-social-login";

import {
DASHBOARD_ADMIN_EXECUTION,
Expand All @@ -27,7 +28,7 @@ export class DashboardComponent implements OnInit {
@ViewChild(HubComponent) hubComponent!: HubComponent;

isAdmin: boolean = this.userService.isAdmin();
isLogin = this.userService.isLogin();
isLogin: boolean = this.userService.isLogin();
displayForum: boolean = true;
displayNavbar: boolean = true;
isCollpased: boolean = false;
Expand All @@ -47,13 +48,12 @@ export class DashboardComponent implements OnInit {
private router: Router,
private flarumService: FlarumService,
private cdr: ChangeDetectorRef,
private ngZone: NgZone
private ngZone: NgZone,
private socialAuthService: SocialAuthService,
private route: ActivatedRoute
) {}

ngOnInit(): void {
this.isLogin = this.userService.isLogin();
this.isAdmin = this.userService.isAdmin();

this.isCollpased = false;

this.router.events.pipe(untilDestroyed(this)).subscribe(() => {
Expand All @@ -78,6 +78,17 @@ export class DashboardComponent implements OnInit {
this.cdr.detectChanges();
});
});

this.socialAuthService.authState.pipe(untilDestroyed(this)).subscribe(user => {
this.userService
.googleLogin(user.idToken)
.pipe(untilDestroyed(this))
.subscribe(() => {
this.ngZone.run(() => {
this.router.navigateByUrl(this.route.snapshot.queryParams["returnUrl"] || DASHBOARD_USER_WORKFLOW);
});
});
});
}

forumLogin() {
Expand Down

This file was deleted.

Loading

0 comments on commit 58a214f

Please sign in to comment.