Skip to content

Commit 4faa78c

Browse files
GspikeHalokunwp1aglinxinyuan
authored andcommitted
Fix Google Login button disappearing issue (#3197)
### 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]>
1 parent de3451c commit 4faa78c

File tree

12 files changed

+207
-88
lines changed

12 files changed

+207
-88
lines changed

core/amber/src/main/scala/edu/uci/ics/texera/web/resource/auth/GoogleAuthResource.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ class GoogleAuthResource {
3737

3838
@GET
3939
@Path("/clientid")
40-
def getClientId: String = {
41-
clientId
42-
}
40+
def getClientId: String = clientId
4341

4442
@POST
4543
@Consumes(Array(MediaType.TEXT_PLAIN))

core/gui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
},
2222
"private": true,
2323
"dependencies": {
24+
"@abacritt/angularx-social-login": "2.1.0",
2425
"@ali-hm/angular-tree-component": "12.0.5",
2526
"@angular/animations": "16.2.12",
2627
"@angular/cdk": "16.2.12",

core/gui/src/app/app.module.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,13 +133,16 @@ import { SearchBarComponent } from "./dashboard/component/user/search-bar/search
133133
import { ListItemComponent } from "./dashboard/component/user/list-item/list-item.component";
134134
import { HubComponent } from "./hub/component/hub.component";
135135
import { HubWorkflowSearchComponent } from "./hub/component/workflow/search/hub-workflow-search.component";
136-
import { GoogleLoginComponent } from "./dashboard/component/user/google-login/google-login.component";
137136
import { HubWorkflowComponent } from "./hub/component/workflow/hub-workflow.component";
138137
import { HubWorkflowDetailComponent } from "./hub/component/workflow/detail/hub-workflow-detail.component";
139138
import { LandingPageComponent } from "./hub/component/landing-page/landing-page.component";
140139
import { BrowseSectionComponent } from "./hub/component/browse-section/browse-section.component";
141140
import { BreakpointConditionInputComponent } from "./workspace/component/code-editor-dialog/breakpoint-condition-input/breakpoint-condition-input.component";
142141
import { CodeDebuggerComponent } from "./workspace/component/code-editor-dialog/code-debugger.component";
142+
import { GoogleAuthService } from "./common/service/user/google-auth.service";
143+
import { SocialLoginModule, SocialAuthServiceConfig, GoogleSigninButtonModule } from "@abacritt/angularx-social-login";
144+
import { GoogleLoginProvider } from "@abacritt/angularx-social-login";
145+
import { lastValueFrom } from "rxjs";
143146

144147
registerLocaleData(en);
145148

@@ -225,7 +228,6 @@ registerLocaleData(en);
225228
HubWorkflowComponent,
226229
HubWorkflowSearchComponent,
227230
HubWorkflowDetailComponent,
228-
GoogleLoginComponent,
229231
LandingPageComponent,
230232
BrowseSectionComponent,
231233
BreakpointConditionInputComponent,
@@ -289,6 +291,8 @@ registerLocaleData(en);
289291
NzTreeViewModule,
290292
NzNoAnimationModule,
291293
TreeModule,
294+
SocialLoginModule,
295+
GoogleSigninButtonModule,
292296
],
293297
providers: [
294298
provideNzI18n(en_US),
@@ -303,6 +307,20 @@ registerLocaleData(en);
303307
useClass: BlobErrorHttpInterceptor,
304308
multi: true,
305309
},
310+
{
311+
provide: "SocialAuthServiceConfig",
312+
useFactory: (googleAuthService: GoogleAuthService, userService: UserService) => {
313+
return lastValueFrom(googleAuthService.getClientId()).then(clientId => ({
314+
providers: [
315+
{
316+
id: GoogleLoginProvider.PROVIDER_ID,
317+
provider: new GoogleLoginProvider(clientId, { oneTapEnabled: !userService.isLogin() }),
318+
},
319+
],
320+
})) as Promise<SocialAuthServiceConfig>;
321+
},
322+
deps: [GoogleAuthService, UserService],
323+
},
306324
],
307325
bootstrap: [AppComponent],
308326
})

core/gui/src/app/common/service/user/auth.service.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,13 @@ export class AuthService {
6161
public googleAuth(credential: string): Observable<Readonly<{ accessToken: string }>> {
6262
return this.http.post<Readonly<{ accessToken: string }>>(
6363
`${AppSettings.getApiEndpoint()}/${AuthService.GOOGLE_LOGIN_ENDPOINT}`,
64-
`${credential}`
64+
credential,
65+
{
66+
headers: {
67+
"Content-Type": "text/plain",
68+
Accept: "application/json",
69+
},
70+
}
6571
);
6672
}
6773

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,15 @@
11
import { Injectable } from "@angular/core";
2-
import { Subject } from "rxjs";
2+
import { Observable } from "rxjs";
33
import { HttpClient } from "@angular/common/http";
44
import { AppSettings } from "../../app-setting";
5-
declare var window: any;
6-
export interface CredentialResponse {
7-
client_id: string;
8-
credential: string;
9-
select_by: string;
10-
}
5+
116
@Injectable({
127
providedIn: "root",
138
})
149
export class GoogleAuthService {
15-
private _googleCredentialResponse = new Subject<CredentialResponse>();
1610
constructor(private http: HttpClient) {}
17-
public googleAuthInit(parent: HTMLElement | null) {
18-
this.http.get(`${AppSettings.getApiEndpoint()}/auth/google/clientid`, { responseType: "text" }).subscribe({
19-
next: response => {
20-
window.onGoogleLibraryLoad = () => {
21-
window.google.accounts.id.initialize({
22-
client_id: response,
23-
callback: (auth: CredentialResponse) => {
24-
this._googleCredentialResponse.next(auth);
25-
},
26-
});
27-
window.google.accounts.id.renderButton(parent, { width: 200 });
28-
window.google.accounts.id.prompt();
29-
};
30-
},
31-
error: (err: unknown) => {
32-
console.error(err);
33-
},
34-
});
35-
}
3611

37-
get googleCredentialResponse() {
38-
return this._googleCredentialResponse.asObservable();
12+
getClientId(): Observable<string> {
13+
return this.http.get(`${AppSettings.getApiEndpoint()}/auth/google/clientid`, { responseType: "text" });
3914
}
4015
}

core/gui/src/app/dashboard/component/dashboard.component.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,17 @@
141141
<div class="page-container">
142142
<nz-layout>
143143
<div
144-
*ngIf="displayNavbar"
144+
[hidden]="!displayNavbar"
145145
id="nav">
146146
<texera-search-bar></texera-search-bar>
147147
<ng-container *ngIf="isLogin">
148148
<texera-user-icon></texera-user-icon>
149149
</ng-container>
150-
<texera-google-login [hidden]="isLogin"></texera-google-login>
150+
<asl-google-signin-button
151+
*ngIf="!isLogin"
152+
type="standard"
153+
size="large"
154+
width="200"></asl-google-signin-button>
151155
</div>
152156

153157
<nz-content>

core/gui/src/app/dashboard/component/dashboard.component.scss

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ texera-search-bar {
1717
padding-right: 10px;
1818
}
1919

20-
texera-google-login {
21-
float: right;
22-
padding: 5px 0;
23-
}
24-
2520
texera-user-icon {
2621
padding: 0 24px;
2722
}
@@ -70,3 +65,9 @@ nz-content {
7065
.hidden {
7166
display: none;
7267
}
68+
69+
#nav {
70+
max-width: 100%;
71+
max-height: 100%;
72+
overflow: hidden;
73+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { TestBed, ComponentFixture } from "@angular/core/testing";
2+
import { DashboardComponent } from "./dashboard.component";
3+
import { NO_ERRORS_SCHEMA, ChangeDetectorRef, NgZone, EventEmitter } from "@angular/core";
4+
import { By } from "@angular/platform-browser";
5+
import { of } from "rxjs";
6+
7+
import { UserService } from "../../common/service/user/user.service";
8+
import { FlarumService } from "../service/user/flarum/flarum.service";
9+
import { SocialAuthService } from "@abacritt/angularx-social-login";
10+
import {
11+
Router,
12+
NavigationEnd,
13+
ActivatedRoute,
14+
ActivatedRouteSnapshot,
15+
UrlSegment,
16+
Params,
17+
Data,
18+
} from "@angular/router";
19+
import { convertToParamMap } from "@angular/router";
20+
21+
describe("DashboardComponent", () => {
22+
let component: DashboardComponent;
23+
let fixture: ComponentFixture<DashboardComponent>;
24+
25+
let userServiceMock: Partial<UserService>;
26+
let routerMock: Partial<Router>;
27+
let flarumServiceMock: Partial<FlarumService>;
28+
let cdrMock: Partial<ChangeDetectorRef>;
29+
let ngZoneMock: Partial<NgZone>;
30+
let socialAuthServiceMock: Partial<SocialAuthService>;
31+
let activatedRouteMock: Partial<ActivatedRoute>;
32+
33+
const activatedRouteSnapshotMock: Partial<ActivatedRouteSnapshot> = {
34+
queryParams: {},
35+
url: [] as UrlSegment[],
36+
params: {} as Params,
37+
fragment: null,
38+
data: {} as Data,
39+
paramMap: convertToParamMap({}),
40+
queryParamMap: convertToParamMap({}),
41+
outlet: "",
42+
routeConfig: null,
43+
root: null as any,
44+
parent: null as any,
45+
firstChild: null as any,
46+
children: [],
47+
pathFromRoot: [],
48+
};
49+
50+
beforeEach(async () => {
51+
userServiceMock = {
52+
isAdmin: jasmine.createSpy("isAdmin").and.returnValue(false),
53+
isLogin: jasmine.createSpy("isLogin").and.returnValue(false),
54+
userChanged: jasmine.createSpy("userChanged").and.returnValue(of(null)),
55+
};
56+
57+
routerMock = {
58+
events: of(new NavigationEnd(1, "/dashboard", "/dashboard")),
59+
url: "/dashboard",
60+
navigateByUrl: jasmine.createSpy("navigateByUrl"),
61+
};
62+
63+
flarumServiceMock = {
64+
auth: jasmine.createSpy("auth").and.returnValue(of({ token: "dummyToken" })),
65+
register: jasmine.createSpy("register").and.returnValue(of(null)),
66+
};
67+
68+
cdrMock = {
69+
detectChanges: jasmine.createSpy("detectChanges"),
70+
};
71+
72+
ngZoneMock = {
73+
hasPendingMicrotasks: false,
74+
hasPendingMacrotasks: false,
75+
onUnstable: new EventEmitter<any>(),
76+
onMicrotaskEmpty: new EventEmitter<any>(),
77+
onStable: new EventEmitter<any>(),
78+
onError: new EventEmitter<any>(),
79+
run: (fn: () => any) => fn(),
80+
runGuarded: (fn: () => any) => fn(),
81+
runOutsideAngular: (fn: () => any) => fn(),
82+
runTask: (fn: () => any) => fn(),
83+
};
84+
85+
socialAuthServiceMock = {
86+
authState: of(),
87+
};
88+
89+
activatedRouteMock = {
90+
snapshot: activatedRouteSnapshotMock as ActivatedRouteSnapshot,
91+
};
92+
93+
await TestBed.configureTestingModule({
94+
declarations: [DashboardComponent],
95+
providers: [
96+
{ provide: UserService, useValue: userServiceMock },
97+
{ provide: Router, useValue: routerMock },
98+
{ provide: FlarumService, useValue: flarumServiceMock },
99+
{ provide: ChangeDetectorRef, useValue: cdrMock },
100+
{ provide: NgZone, useValue: ngZoneMock },
101+
{ provide: SocialAuthService, useValue: socialAuthServiceMock },
102+
{ provide: ActivatedRoute, useValue: activatedRouteMock },
103+
],
104+
schemas: [NO_ERRORS_SCHEMA],
105+
}).compileComponents();
106+
});
107+
108+
beforeEach(() => {
109+
fixture = TestBed.createComponent(DashboardComponent);
110+
component = fixture.componentInstance;
111+
fixture.detectChanges();
112+
});
113+
114+
it("should create the component", () => {
115+
expect(component).toBeTruthy();
116+
});
117+
118+
it("should render Google sign-in button when user is NOT logged in", () => {
119+
(userServiceMock.isLogin as jasmine.Spy).and.returnValue(false);
120+
fixture.detectChanges();
121+
122+
const googleSignInBtn = fixture.debugElement.query(By.css("asl-google-signin-button"));
123+
expect(googleSignInBtn).toBeTruthy();
124+
});
125+
});

core/gui/src/app/dashboard/component/dashboard.component.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { UserService } from "../../common/service/user/user.service";
33
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
44
import { FlarumService } from "../service/user/flarum/flarum.service";
55
import { HttpErrorResponse } from "@angular/common/http";
6-
import { NavigationEnd, Router } from "@angular/router";
6+
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
77
import { HubComponent } from "../../hub/component/hub.component";
8+
import { SocialAuthService } from "@abacritt/angularx-social-login";
89

910
import {
1011
DASHBOARD_ADMIN_EXECUTION,
@@ -27,7 +28,7 @@ export class DashboardComponent implements OnInit {
2728
@ViewChild(HubComponent) hubComponent!: HubComponent;
2829

2930
isAdmin: boolean = this.userService.isAdmin();
30-
isLogin = this.userService.isLogin();
31+
isLogin: boolean = this.userService.isLogin();
3132
displayForum: boolean = true;
3233
displayNavbar: boolean = true;
3334
isCollpased: boolean = false;
@@ -47,13 +48,12 @@ export class DashboardComponent implements OnInit {
4748
private router: Router,
4849
private flarumService: FlarumService,
4950
private cdr: ChangeDetectorRef,
50-
private ngZone: NgZone
51+
private ngZone: NgZone,
52+
private socialAuthService: SocialAuthService,
53+
private route: ActivatedRoute
5154
) {}
5255

5356
ngOnInit(): void {
54-
this.isLogin = this.userService.isLogin();
55-
this.isAdmin = this.userService.isAdmin();
56-
5757
this.isCollpased = false;
5858

5959
this.router.events.pipe(untilDestroyed(this)).subscribe(() => {
@@ -78,6 +78,17 @@ export class DashboardComponent implements OnInit {
7878
this.cdr.detectChanges();
7979
});
8080
});
81+
82+
this.socialAuthService.authState.pipe(untilDestroyed(this)).subscribe(user => {
83+
this.userService
84+
.googleLogin(user.idToken)
85+
.pipe(untilDestroyed(this))
86+
.subscribe(() => {
87+
this.ngZone.run(() => {
88+
this.router.navigateByUrl(this.route.snapshot.queryParams["returnUrl"] || DASHBOARD_USER_WORKFLOW);
89+
});
90+
});
91+
});
8192
}
8293

8394
forumLogin() {

core/gui/src/app/dashboard/component/user/google-login/google-login.component.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

0 commit comments

Comments
 (0)