diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 86ab11a374a..780ad8aba91 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { Router } from "@angular/router"; -import { merge, of, Subject } from "rxjs"; +import { BehaviorSubject, merge, of, Subject } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; @@ -111,6 +111,9 @@ import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/imp import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service"; import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { AchievementHub } from "@bitwarden/common/tools/achievements/achievement-hub"; +import { TotallyAttachedValidator, TotallyAttachedAchievement } from "@bitwarden/common/tools/achievements/examples/example-validators"; +import {AchievementEvent, AchievementValidator, UserActionEvent } from "@bitwarden/common/tools/achievements/types"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -192,6 +195,8 @@ const DISK_BACKUP_LOCAL_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService >("DISK_BACKUP_LOCAL_STORAGE"); +const events$ = new Subject(); + /** * Provider definitions used in the ngModule. * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. @@ -662,6 +667,18 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultSshImportPromptService, deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction], }), + safeProvider({ + provide: AchievementHub, + useFactory: ( + ) => { + const validators$ = new BehaviorSubject([TotallyAttachedValidator]); + const achievements$ = new Subject(); + const hub = new AchievementHub(validators$, events$, achievements$); + return hub; + }, + deps: [ + ], + }), ]; @NgModule({ diff --git a/apps/browser/src/tools/popup/achievements/achievements.component.ts b/apps/browser/src/tools/popup/achievements/achievements.component.ts index 9c3ed161458..8f3c04e02c3 100644 --- a/apps/browser/src/tools/popup/achievements/achievements.component.ts +++ b/apps/browser/src/tools/popup/achievements/achievements.component.ts @@ -5,6 +5,7 @@ import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AchievementsListComponent } from "@bitwarden/angular/tools/achievements/achievements-list.component"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AchievementHub } from "@bitwarden/common/tools/achievements/achievement-hub"; import { EventStoreAbstraction } from "@bitwarden/common/tools/achievements/event-store.abstraction.service"; import { VaultItems_10_Added_Achievement } from "@bitwarden/common/tools/achievements/examples/achievements"; import { AchievementEarnedEvent, AchievementId } from "@bitwarden/common/tools/achievements/types"; @@ -37,6 +38,7 @@ export class AchievementsComponent implements OnInit { constructor( private eventStore: EventStoreAbstraction, private accountService: AccountService, + private achievementHub: AchievementHub ) {} async ngOnInit() { @@ -63,6 +65,7 @@ export class AchievementsComponent implements OnInit { achievement: { type: "earned", name: VaultItems_10_Added_Achievement.name as AchievementId }, }; - this.eventStore.addEvent(earnedAchievement); + // this.eventStore.addEvent(earnedAchievement); + this.achievementHub.addEvent(earnedAchievement); } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index daec9897d3b..c9747a060df 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -248,9 +248,9 @@ import { EventCollectionService } from "@bitwarden/common/services/event/event-c import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { AchievementService as AchievementServiceAbstraction } from "@bitwarden/common/tools/achievements/achievement.service.abstraction"; -import { DefaultAchievementService } from "@bitwarden/common/tools/achievements/default-achievement.service"; import { EventStore } from "@bitwarden/common/tools/achievements/event-store"; import { EventStoreAbstraction } from "@bitwarden/common/tools/achievements/event-store.abstraction.service"; +import { HubAchievementService } from "@bitwarden/common/tools/achievements/hub-achievement.service"; import { PasswordStrengthService, PasswordStrengthServiceAbstraction, @@ -359,6 +359,7 @@ import { ENV_ADDITIONAL_REGIONS, } from "./injection-tokens"; import { ModalService } from "./modal.service"; +import { AchievementHub } from "@bitwarden/common/tools/achievements/achievement-hub"; /** * Provider definitions used in the ngModule. @@ -1503,8 +1504,8 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: AchievementServiceAbstraction, - useClass: DefaultAchievementService, - deps: [EventStoreAbstraction], + useClass: HubAchievementService, + deps: [], }), safeProvider({ provide: AchievementNotifierServiceAbstraction, diff --git a/libs/angular/src/tools/achievements/achievement-notifier.service.ts b/libs/angular/src/tools/achievements/achievement-notifier.service.ts index 5f1ae302eaa..ac0b315fecf 100644 --- a/libs/angular/src/tools/achievements/achievement-notifier.service.ts +++ b/libs/angular/src/tools/achievements/achievement-notifier.service.ts @@ -32,9 +32,30 @@ export class AchievementNotifierService implements AchievementNotifierServiceAbs * Invoke showing toast */ // FIXME getClientType browswer and achievementEarned.service.name.extension won't match - const account = await firstValueFrom(this.accountService.activeAccount$); + // const account = await firstValueFrom(this.accountService.activeAccount$); + // this.achievementService + // .achievementsEarned$(account.id) + // .pipe( + // // Removing filter for testing purposes + // // filter(achievementEarned => achievementEarned.service.name == this.platformUtilsService.getClientType())).pipe( + // switchMap((earned) => this.achievementService.achievementById$(earned.achievement.name)), + // tap((achievement) => { + // //eslint-disable-next-line no-console + // console.log(achievement); + // }), + // ) + // .subscribe((achievement) => { + // this.toastService.showToast({ + // variant: "info", + // title: achievement.name, + // message: achievement.description, + // icon: AchievementIcon, + // }); + // }); + + // FIXME Migrate to use achievementHub.earned$() instead of achievementService.achievementsEarned$ this.achievementService - .achievementsEarned$(account.id) + .earned$ .pipe( // Removing filter for testing purposes // filter(achievementEarned => achievementEarned.service.name == this.platformUtilsService.getClientType())).pipe( @@ -52,7 +73,5 @@ export class AchievementNotifierService implements AchievementNotifierServiceAbs icon: AchievementIcon, }); }); - - // FIXME Migrate to use achievementHub.earned$() instead of achievementService.achievementsEarned$ } } diff --git a/libs/common/src/tools/achievements/achievement-hub.ts b/libs/common/src/tools/achievements/achievement-hub.ts index ec9366c7aac..55210eec7a7 100644 --- a/libs/common/src/tools/achievements/achievement-hub.ts +++ b/libs/common/src/tools/achievements/achievement-hub.ts @@ -104,4 +104,9 @@ export class AchievementHub { startWith(new Map()), ); } + + //Test methods + addEvent(event: AchievementEvent) { + this.achievementLog.next(event); + } } diff --git a/libs/common/src/tools/achievements/achievement.service.abstraction.ts b/libs/common/src/tools/achievements/achievement.service.abstraction.ts index f1e67ea3a01..6e45f633ea2 100644 --- a/libs/common/src/tools/achievements/achievement.service.abstraction.ts +++ b/libs/common/src/tools/achievements/achievement.service.abstraction.ts @@ -9,4 +9,8 @@ export abstract class AchievementService { abstract achievementsEarned$: (userId: UserId) => Observable; abstract achievementsInProgress$: (userId: UserId) => Observable; + + abstract earned$: Observable; + abstract inProgress$: Observable; + } diff --git a/libs/common/src/tools/achievements/default-achievement.service.ts b/libs/common/src/tools/achievements/default-achievement.service.ts index c4e25bce87a..b1a051478d6 100644 --- a/libs/common/src/tools/achievements/default-achievement.service.ts +++ b/libs/common/src/tools/achievements/default-achievement.service.ts @@ -1,7 +1,8 @@ -import { filter, find, from, Observable } from "rxjs"; +import { filter, find, from, map, Observable } from "rxjs"; import { UserId } from "@bitwarden/common/types/guid"; +import { AchievementHub } from "./achievement-hub"; import { AchievementService as AchievementServiceAbstraction } from "./achievement.service.abstraction"; import { EventStoreAbstraction } from "./event-store.abstraction.service"; import { @@ -32,10 +33,16 @@ export class DefaultAchievementService implements AchievementServiceAbstraction // Provided by the AchievementHub achievementsEarned$: (userId: UserId) => Observable; + // Provided by the AchievementHub + earned$: Observable; + // Provided by the AchievementHub achievementsInProgress$: (userId: UserId) => Observable; - constructor(protected eventStore: EventStoreAbstraction) { + // Provided by the AchievementHub + inProgress$: Observable; + + constructor(protected eventStore: EventStoreAbstraction, protected achievementHub: AchievementHub) { this.achievementById$ = (achievementId: AchievementId) => this._achievementsSubject.pipe(find((item: Achievement) => item.name === achievementId)); @@ -56,5 +63,14 @@ export class DefaultAchievementService implements AchievementServiceAbstraction ), ); }; + + this.earned$ = this.achievementHub.new$().pipe(filter((event) => isEarnedEvent(event)), map((event) => { + return event as AchievementEarnedEvent; + })); + + this.inProgress$ = this.achievementHub.new$().pipe(filter((event) => isProgressEvent(event)), map((event) => { + return event as AchievementProgressEvent; + })); + } } diff --git a/libs/common/src/tools/achievements/hub-achievement.service.ts b/libs/common/src/tools/achievements/hub-achievement.service.ts new file mode 100644 index 00000000000..d2daaad55db --- /dev/null +++ b/libs/common/src/tools/achievements/hub-achievement.service.ts @@ -0,0 +1,52 @@ +import { filter, find, from, map, Observable } from "rxjs"; + +import { UserId } from "@bitwarden/common/types/guid"; + +import { AchievementHub } from "./achievement-hub"; +import { AchievementService as AchievementServiceAbstraction } from "./achievement.service.abstraction"; +import { EventStoreAbstraction } from "./event-store.abstraction.service"; +import { + VaultItems_1_Added_Achievement, + VaultItems_10_Added_Achievement, +} from "./examples/achievements"; +import { isEarnedEvent, isProgressEvent } from "./meta"; +import { + Achievement, + AchievementEarnedEvent, + AchievementEvent, + AchievementId, + AchievementProgressEvent, +} from "./types"; + +// Service might be deprecated in favor of the AchievmentHub +// The hub is currently missing a way of listing all achievements, finding by id, but that could be possibly done via the AchievementManager +export class HubAchievementService implements AchievementServiceAbstraction { + private _achievements: Achievement[] = [ + VaultItems_1_Added_Achievement, + VaultItems_10_Added_Achievement, + ]; + + private _achievementsSubject = from(this._achievements); + + earned$: Observable; + inProgress$: Observable; + + achievementById$: (achievementId: string) => Observable; + achievementsEarned$ = (userId: UserId) => { return this.earned$ }; + achievementsInProgress$ = (userId: UserId) => { return this.inProgress$ } + + private achievementHub = new AchievementHub(); + + constructor() { + this.achievementById$ = (achievementId: AchievementId) => + this._achievementsSubject.pipe(find((item: Achievement) => item.name === achievementId)); + + this.earned$ = this.achievementHub.new$().pipe(filter((event) => isEarnedEvent(event)), map((event) => { + return event as AchievementEarnedEvent; + })); + + this.inProgress$ = this.achievementHub.new$().pipe(filter((event) => isProgressEvent(event)), map((event) => { + return event as AchievementProgressEvent; + })); + } +}