Skip to content

Commit 3ba45b9

Browse files
committed
fix: grace period not updated
1 parent f12470c commit 3ba45b9

File tree

6 files changed

+88
-70
lines changed

6 files changed

+88
-70
lines changed

src/app/app.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ describe('AppComponent', () => {
105105
provide: AppUserService, useValue: {
106106
updateUserProperties: vi.fn().mockReturnValue(Promise.resolve()),
107107
getSubscriptionRole: vi.fn().mockReturnValue(Promise.resolve('free')),
108-
getGracePeriodUntil: vi.fn().mockReturnValue(of(null)),
108+
gracePeriodUntil: signal(null),
109109
isAdmin: vi.fn().mockReturnValue(Promise.resolve(false))
110110
}
111111
},

src/app/components/grace-period-banner/grace-period-banner.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<ng-container *ngIf="(gracePeriodUntil$ | async) as gracePeriodUntil">
1+
<ng-container *ngIf="gracePeriodUntil() as gracePeriodUntil">
22
<div #bannerElement *ngIf="!isDismissed" class="grace-period-banner mat-typography">
33
<mat-icon class="banner-warning-icon">warning</mat-icon>
44
<span class="banner-text">

src/app/components/grace-period-banner/grace-period-banner.component.spec.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
33
import { GracePeriodBannerComponent } from './grace-period-banner.component';
44
import { AppUserService } from '../../services/app.user.service';
55
import { of } from 'rxjs';
6+
import { signal } from '@angular/core';
67
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
78
import { RouterTestingModule } from '@angular/router/testing';
89

@@ -11,7 +12,7 @@ describe('GracePeriodBannerComponent', () => {
1112
let fixture: ComponentFixture<GracePeriodBannerComponent>;
1213

1314
const mockUserService = {
14-
getGracePeriodUntil: vi.fn().mockReturnValue(of(null))
15+
gracePeriodUntil: signal<Date | null>(null)
1516
};
1617

1718
beforeEach(async () => {
@@ -41,9 +42,8 @@ describe('GracePeriodBannerComponent', () => {
4142

4243
it('should show banner when grace period date is present', async () => {
4344
const mockDate = new Date();
44-
mockUserService.getGracePeriodUntil.mockReturnValue(of(mockDate));
45+
mockUserService.gracePeriodUntil.set(mockDate);
4546

46-
component.ngOnInit();
4747
fixture.detectChanges();
4848
await fixture.whenStable();
4949

@@ -53,9 +53,8 @@ describe('GracePeriodBannerComponent', () => {
5353
});
5454

5555
it('should hide banner when grace period date is null', () => {
56-
mockUserService.getGracePeriodUntil.mockReturnValue(of(null));
56+
mockUserService.gracePeriodUntil.set(null);
5757

58-
component.ngOnInit();
5958
fixture.detectChanges();
6059

6160
const banner = fixture.nativeElement.querySelector('.grace-period-banner');
@@ -64,9 +63,8 @@ describe('GracePeriodBannerComponent', () => {
6463

6564
it('should hide banner when dismissed', async () => {
6665
const mockDate = new Date();
67-
mockUserService.getGracePeriodUntil.mockReturnValue(of(mockDate));
66+
mockUserService.gracePeriodUntil.set(mockDate);
6867

69-
component.ngOnInit();
7068
fixture.detectChanges();
7169
await fixture.whenStable();
7270

src/app/components/grace-period-banner/grace-period-banner.component.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, OnInit, Output, EventEmitter, ViewChild, ElementRef, AfterViewInit, OnDestroy } from '@angular/core';
1+
import { Component, OnInit, Output, EventEmitter, ViewChild, ElementRef, AfterViewInit, OnDestroy, effect, inject } from '@angular/core';
22
import { Observable, Subscription } from 'rxjs';
33
import { AppUserService } from '../../services/app.user.service';
44
import { LoggerService } from '../../services/logger.service';
@@ -29,21 +29,18 @@ export class GracePeriodBannerComponent implements OnInit, AfterViewInit, OnDest
2929
return this._bannerElement;
3030
}
3131

32-
gracePeriodUntil$!: Observable<Date | null>;
32+
private userService = inject(AppUserService);
33+
private logger = inject(LoggerService);
34+
35+
gracePeriodUntil = this.userService.gracePeriodUntil;
3336
isDismissed = false;
3437
private subscription: Subscription | null = null;
3538
private resizeObserver: ResizeObserver | null = null;
3639

37-
constructor(private userService: AppUserService, private logger: LoggerService) { }
38-
39-
ngOnInit(): void {
40-
this.logger.info('[GracePeriodBanner] ngOnInit - Getting grace period observable');
41-
this.gracePeriodUntil$ = this.userService.getGracePeriodUntil();
42-
}
43-
44-
ngAfterViewInit(): void {
45-
// Subscribe to grace period changes to update height when banner appears
46-
this.subscription = this.gracePeriodUntil$.subscribe(() => {
40+
constructor() {
41+
effect(() => {
42+
const date = this.gracePeriodUntil();
43+
this.logger.info('[GracePeriodBanner] Effect triggered - gracePeriodUntil:', date);
4744
// Use setTimeout to ensure the DOM is updated before measuring
4845
setTimeout(() => {
4946
this.updateHeight();
@@ -52,6 +49,18 @@ export class GracePeriodBannerComponent implements OnInit, AfterViewInit, OnDest
5249
});
5350
}
5451

52+
ngOnInit(): void {
53+
this.logger.info('[GracePeriodBanner] ngOnInit - Signal already initialized in service');
54+
}
55+
56+
ngAfterViewInit(): void {
57+
// Use an effect or similar to update height when signal changes
58+
// Since we are in EXECUTION and the component might be simple,
59+
// using effect() in constructor is cleaner for signals.
60+
// But for this refactor, I'll just change the subscription to an effect-like behavior if needed.
61+
// Actually, let's use effect in the constructor for the height update.
62+
}
63+
5564
ngOnDestroy(): void {
5665
this.subscription?.unsubscribe();
5766
this.resizeObserver?.disconnect();

src/app/services/app.user.service.spec.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ vi.mock('@angular/fire/auth', async (importOriginal) => {
2020

2121
vi.mock('@angular/fire/firestore', async (importOriginal) => {
2222
const actual: any = await importOriginal();
23+
const { of } = await import('rxjs');
2324
return {
2425
...actual,
2526
doc: vi.fn().mockReturnValue({}),
26-
docData: vi.fn(),
27+
docData: vi.fn().mockReturnValue(of({})),
2728
setDoc: vi.fn().mockResolvedValue(undefined),
2829
updateDoc: vi.fn().mockResolvedValue(undefined),
2930
};
@@ -63,10 +64,10 @@ describe('AppUserService', () => {
6364
{ provide: AppWindowService, useValue: {} }
6465
]
6566
});
66-
service = TestBed.inject(AppUserService);
6767
});
6868

6969
it('should be created', () => {
70+
service = TestBed.inject(AppUserService);
7071
expect(service).toBeTruthy();
7172
});
7273

@@ -79,6 +80,7 @@ describe('AppUserService', () => {
7980

8081
// Note: because authState is mocked to return the user, we need to ensure firstValueFrom works
8182
// But AppUserService.getSubscriptionRole uses authState(this.auth)
83+
service = TestBed.inject(AppUserService);
8284
});
8385

8486
it('should return basic role', async () => {
@@ -108,43 +110,47 @@ describe('AppUserService', () => {
108110
mockAuth.currentUser.getIdTokenResult.mockReturnValue(Promise.resolve({
109111
claims: { stripeRole: 'pro' }
110112
}));
113+
service = TestBed.inject(AppUserService);
111114
const hasAccess = await service.hasPaidAccess();
112115
expect(hasAccess).toBe(true);
113116
});
114117
});
115118

116-
describe('getGracePeriodUntil', () => {
119+
describe('gracePeriodUntil signal', () => {
117120
it('should return null if user is not logged in', async () => {
118-
mockAuth.currentUser = null;
119-
const res = await firstValueFrom(service.getGracePeriodUntil());
120-
expect(res).toBeNull();
121+
(authState as any).mockReturnValue(of(null));
122+
service = TestBed.inject(AppUserService);
123+
expect(service.gracePeriodUntil()).toBeNull();
121124
});
122125

123126
it('should return null if no grace period is set', async () => {
124-
mockAuth.currentUser = {
127+
(authState as any).mockReturnValue(of({
125128
uid: 'u1',
126129
getIdTokenResult: vi.fn().mockResolvedValue({ claims: {} })
127-
};
130+
}));
128131
(docData as any).mockReturnValue(of({}));
129-
const res = await firstValueFrom(service.getGracePeriodUntil());
130-
expect(res).toBeNull();
132+
service = TestBed.inject(AppUserService);
133+
expect(service.gracePeriodUntil()).toBeNull();
131134
});
132135

133136
it('should return Date if grace period is set', async () => {
134137
const mockDate = new Date();
135-
mockAuth.currentUser = {
138+
(authState as any).mockReturnValue(of({
136139
uid: 'u1',
137140
getIdTokenResult: vi.fn().mockResolvedValue({ claims: {} })
138-
};
141+
}));
139142
(docData as any).mockReturnValue(of({
140143
gracePeriodUntil: { toDate: () => mockDate }
141144
}));
142145

143-
const res = await firstValueFrom(service.getGracePeriodUntil());
144-
expect(res).toEqual(mockDate);
146+
service = TestBed.inject(AppUserService);
147+
expect(service.gracePeriodUntil()).toEqual(mockDate);
145148
});
146149
});
147150
describe('updateUserProperties', () => {
151+
beforeEach(() => {
152+
service = TestBed.inject(AppUserService);
153+
});
148154
it('should split settings and other properties', async () => {
149155
const user = { uid: 'u1' } as any;
150156
const settings = { theme: 'dark' };
@@ -216,6 +222,9 @@ describe('AppUserService', () => {
216222
});
217223

218224
describe('static user role checks', () => {
225+
beforeEach(() => {
226+
service = TestBed.inject(AppUserService);
227+
});
219228
const mockUser = { uid: 'u1' } as any;
220229

221230
describe('isProUser', () => {
@@ -302,6 +311,9 @@ describe('AppUserService', () => {
302311
});
303312
});
304313
describe('deleteAllUserData', () => {
314+
beforeEach(() => {
315+
service = TestBed.inject(AppUserService);
316+
});
305317
it('should call deleteSelf cloud function and sign out', async () => {
306318
await service.deleteAllUserData({ uid: 'u1' } as any);
307319

@@ -319,7 +331,10 @@ describe('AppUserService', () => {
319331
});
320332
});
321333

322-
describe('Service Integrations', () => {
334+
describe('Service Integration', () => {
335+
beforeEach(() => {
336+
service = TestBed.inject(AppUserService);
337+
});
323338
const startDate = new Date('2023-01-01');
324339
const endDate = new Date('2023-01-31');
325340

src/app/services/app.user.service.ts

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { inject, Injectable, OnDestroy, EnvironmentInjector, runInInjectionContext } from '@angular/core';
2+
import { toSignal } from '@angular/core/rxjs-interop';
23

34

45
import { Observable, from, firstValueFrom, of, combineLatest, distinctUntilChanged } from 'rxjs';
56
import { StripeRole } from '../models/stripe-role.model';
67
import { User } from '@sports-alliance/sports-lib';
78
import { Privacy } from '@sports-alliance/sports-lib';
89
import { AppEventService } from './app.event.service';
9-
import { catchError, map, take } from 'rxjs/operators';
10+
import { catchError, map, take, switchMap } from 'rxjs/operators';
1011
import {
1112
AppThemes,
1213
UserAppSettingsInterface
@@ -99,6 +100,33 @@ export class AppUserService implements OnDestroy {
99100
private auth = inject(Auth);
100101
private functionsService = inject(AppFunctionsService);
101102
private injector = inject(EnvironmentInjector);
103+
private logger = inject(LoggerService);
104+
private eventService = inject(AppEventService);
105+
private http = inject(HttpClient);
106+
private windowService = inject(AppWindowService);
107+
108+
public readonly gracePeriodUntil = toSignal(
109+
authState(this.auth).pipe(
110+
switchMap(user => {
111+
this.logger.log('[AppUserService] gracePeriodUntil signal init - Current auth user:', user?.uid || 'null');
112+
if (!user) return of(null);
113+
const systemDoc = doc(this.firestore, `users/${user.uid}/system/status`);
114+
return docData(systemDoc).pipe(
115+
map((systemData: any) => {
116+
if (systemData?.gracePeriodUntil) {
117+
return (systemData.gracePeriodUntil as any).toDate();
118+
}
119+
return null;
120+
}),
121+
catchError((error) => {
122+
this.logger.error('[AppUserService] gracePeriodUntil signal error:', error);
123+
return of(null);
124+
})
125+
);
126+
})
127+
),
128+
{ initialValue: null, injector: this.injector }
129+
);
102130

103131
static getDefaultChartTheme(): ChartThemes {
104132
return ChartThemes.Material;
@@ -361,12 +389,7 @@ export class AppUserService implements OnDestroy {
361389
return AppUserService.isProUser(user, isAdmin) || AppUserService.isBasicUser(user);
362390
}
363391

364-
constructor(
365-
private eventService: AppEventService,
366-
private http: HttpClient,
367-
private windowService: AppWindowService,
368-
private logger: LoggerService
369-
) {
392+
constructor() {
370393
authState(this.auth).subscribe((user) => {
371394
if (user) {
372395
this.logger.setUser({ id: user.uid, email: user.email || undefined });
@@ -761,33 +784,6 @@ export class AppUserService implements OnDestroy {
761784
return role === 'pro' || role === 'basic';
762785
}
763786

764-
public getGracePeriodUntil(): Observable<Date | null> {
765-
const user = this.auth.currentUser;
766-
this.logger.log('[AppUserService] getGracePeriodUntil - Current auth user:', user?.uid || 'null');
767-
if (!user) return from([null]);
768-
769-
return runInInjectionContext(this.injector, () => {
770-
// Logic refactored: gracePeriodUntil is now in system/status and merged onto user
771-
// so this can technically just call getUserByID, but that's heavy.
772-
// Let's read directly from system/status for efficiency
773-
const systemDoc = doc(this.firestore, `users/${user.uid}/system/status`);
774-
return docData(systemDoc).pipe(
775-
map((systemData: any) => {
776-
if (systemData?.gracePeriodUntil) {
777-
// Firebase Timestamp to Date
778-
const date = (systemData.gracePeriodUntil as any).toDate();
779-
// this.logger.log('[AppUserService] getGracePeriodUntil - Returning grace period date:', date);
780-
return date;
781-
}
782-
return null;
783-
}),
784-
catchError((error) => {
785-
this.logger.error('[AppUserService] getGracePeriodUntil - Error fetching system document:', error);
786-
return from([null]);
787-
})
788-
);
789-
});
790-
}
791787

792788
public async deleteAllUserData(_user: User) {
793789
try {

0 commit comments

Comments
 (0)