diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 127e07f25e8..4db8952c92c 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -5187,5 +5187,8 @@
},
"changeAtRiskPassword": {
"message": "Change at-risk password"
+ },
+ "achievements": {
+ "message": "Achievements"
}
}
diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html
index de8ab4c7b08..bd8c025db5a 100644
--- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html
+++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html
@@ -13,6 +13,27 @@
+
+
+
+ Achievements
+
+
+
+
+ See all achievements
+
+
+
+
+
+
{{ "availableAccounts" | i18n }}
diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts
index 78bee121afb..c7db6f89812 100644
--- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts
+++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts
@@ -1,6 +1,6 @@
import { CommonModule, Location } from "@angular/common";
import { Component, OnDestroy, OnInit } from "@angular/core";
-import { Router } from "@angular/router";
+import { Router, RouterLink } from "@angular/router";
import { Subject, firstValueFrom, map, of, startWith, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
@@ -50,6 +50,7 @@ import { AccountSwitcherService } from "./services/account-switcher.service";
SectionComponent,
SectionHeaderComponent,
TypographyModule,
+ RouterLink,
],
})
export class AccountSwitcherComponent implements OnInit, OnDestroy {
diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts
index b33940a68d2..05f656f2f4b 100644
--- a/apps/browser/src/popup/app-routing.module.ts
+++ b/apps/browser/src/popup/app-routing.module.ts
@@ -71,6 +71,7 @@ import { NotificationsSettingsComponent } from "../autofill/popup/settings/notif
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
+import { AchievementsComponent } from "../tools/popup/achievements/achievements.component";
import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component";
import { CredentialGeneratorComponent } from "../tools/popup/generator/credential-generator.component";
import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/send-v2/add-edit/send-add-edit.component";
@@ -232,6 +233,12 @@ const routes: Routes = [
canActivate: [unauthGuardFn(unauthRouteOverrides)],
data: { elevation: 1 } satisfies RouteDataProperties,
},
+ {
+ path: "achievements",
+ component: AchievementsComponent,
+ canActivate: [authGuard],
+ data: { elevation: 1 } satisfies RouteDataProperties,
+ },
{
path: "view-cipher",
component: ViewV2Component,
diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts
index fe6fba85a4b..90eff1e9499 100644
--- a/apps/browser/src/popup/services/init.service.ts
+++ b/apps/browser/src/popup/services/init.service.ts
@@ -2,6 +2,7 @@ import { DOCUMENT } from "@angular/common";
import { inject, Inject, Injectable } from "@angular/core";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
+import { AchievementNotifierService } from "@bitwarden/angular/tools/achievements/achievement-notifier.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
@@ -26,6 +27,7 @@ export class InitService {
private logService: LogServiceAbstraction,
private themingService: AbstractThemingService,
private sdkLoadService: SdkLoadService,
+ private achievementNotifierService: AchievementNotifierService,
private viewCacheService: PopupViewCacheService,
@Inject(DOCUMENT) private document: Document,
) {}
@@ -38,6 +40,7 @@ export class InitService {
this.twoFactorService.init();
await this.viewCacheService.init();
await this.sizeService.init();
+ await this.achievementNotifierService.init();
const htmlEl = window.document.documentElement;
this.themingService.applyThemeChangesTo(this.document);
diff --git a/apps/browser/src/tools/popup/achievements/achievements.component.html b/apps/browser/src/tools/popup/achievements/achievements.component.html
new file mode 100644
index 00000000000..73defa852e9
--- /dev/null
+++ b/apps/browser/src/tools/popup/achievements/achievements.component.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/browser/src/tools/popup/achievements/achievements.component.ts b/apps/browser/src/tools/popup/achievements/achievements.component.ts
new file mode 100644
index 00000000000..3d008bdc8ee
--- /dev/null
+++ b/apps/browser/src/tools/popup/achievements/achievements.component.ts
@@ -0,0 +1,66 @@
+import { CommonModule } from "@angular/common";
+import { Component, OnInit } from "@angular/core";
+import { firstValueFrom } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+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";
+import { UserId } from "@bitwarden/common/types/guid";
+import { ButtonModule, IconModule } from "@bitwarden/components";
+
+import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
+import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
+import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
+import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
+
+@Component({
+ templateUrl: "achievements.component.html",
+ standalone: true,
+ imports: [
+ CommonModule,
+ JslibModule,
+ PopupPageComponent,
+ PopupHeaderComponent,
+ PopupFooterComponent,
+ PopOutComponent,
+ ButtonModule,
+ IconModule,
+ ],
+})
+export class AchievementsComponent implements OnInit {
+ private currentUserId: UserId;
+
+ constructor(
+ private eventStore: EventStoreAbstraction,
+ private accountService: AccountService,
+ ) {}
+
+ async ngOnInit() {
+ this.currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
+ }
+
+ testAchievement() {
+ const earnedAchievement: AchievementEarnedEvent = {
+ "@timestamp": Date.now(),
+ event: {
+ kind: "alert",
+ category: "session",
+ },
+ service: {
+ name: "web",
+ type: "client",
+ node: {
+ name: "an-installation-identifier-for-this-client-instance",
+ },
+ environment: "local",
+ version: "2025.3.1-innovation-sprint",
+ },
+ user: { id: this.currentUserId },
+ achievement: { type: "earned", name: VaultItems_10_Added_Achievement.name as AchievementId },
+ };
+
+ this.eventStore.addEvent(earnedAchievement);
+ }
+}
diff --git a/apps/web/src/app/layouts/user-layout.component.html b/apps/web/src/app/layouts/user-layout.component.html
index 6a87658f172..d826301176f 100644
--- a/apps/web/src/app/layouts/user-layout.component.html
+++ b/apps/web/src/app/layouts/user-layout.component.html
@@ -8,6 +8,7 @@
+
diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts
index c531f358b34..80115460376 100644
--- a/apps/web/src/app/oss-routing.module.ts
+++ b/apps/web/src/app/oss-routing.module.ts
@@ -81,6 +81,7 @@ import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-land
import { SMLandingComponent } from "./secrets-manager/secrets-manager-landing/sm-landing.component";
import { DomainRulesComponent } from "./settings/domain-rules.component";
import { PreferencesComponent } from "./settings/preferences.component";
+import { AchievementsComponent } from "./tools/achievements/achievements.component";
import { CredentialGeneratorComponent } from "./tools/credential-generator/credential-generator.component";
import { ReportsModule } from "./tools/reports";
import { AccessComponent, SendAccessExplainerComponent } from "./tools/send/send-access";
@@ -747,6 +748,11 @@ const routes: Routes = [
component: CredentialGeneratorComponent,
data: { titleId: "generator" } satisfies RouteDataProperties,
},
+ {
+ path: "achievements",
+ component: AchievementsComponent,
+ data: { titleId: "achievements" } satisfies RouteDataProperties,
+ },
],
},
{
diff --git a/apps/web/src/app/tools/achievements/achievements.component.html b/apps/web/src/app/tools/achievements/achievements.component.html
new file mode 100644
index 00000000000..a4dab900c46
--- /dev/null
+++ b/apps/web/src/app/tools/achievements/achievements.component.html
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/apps/web/src/app/tools/achievements/achievements.component.ts b/apps/web/src/app/tools/achievements/achievements.component.ts
new file mode 100644
index 00000000000..ce2351346ab
--- /dev/null
+++ b/apps/web/src/app/tools/achievements/achievements.component.ts
@@ -0,0 +1,55 @@
+import { Component, OnInit } from "@angular/core";
+import { firstValueFrom } from "rxjs";
+
+import { AchievementNotifierService } from "@bitwarden/angular/tools/achievements/achievement-notifier.abstraction";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+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";
+import { UserId } from "@bitwarden/common/types/guid";
+
+import { HeaderModule } from "../../layouts/header/header.module";
+import { SharedModule } from "../../shared";
+
+@Component({
+ templateUrl: "achievements.component.html",
+ standalone: true,
+ imports: [SharedModule, HeaderModule],
+})
+export class AchievementsComponent implements OnInit {
+ private currentUserId: UserId;
+
+ constructor(
+ private eventStore: EventStoreAbstraction,
+ private achievementNotifierService: AchievementNotifierService,
+ private accountService: AccountService,
+ ) {}
+
+ async ngOnInit() {
+ await this.achievementNotifierService.init();
+ this.currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
+ }
+
+ testAchievement() {
+ const earnedAchievement: AchievementEarnedEvent = {
+ "@timestamp": Date.now(),
+ event: {
+ kind: "alert",
+ category: "session",
+ },
+ service: {
+ name: "web",
+ type: "client",
+ node: {
+ name: "an-installation-identifier-for-this-client-instance",
+ },
+ environment: "local",
+ version: "2025.3.1-innovation-sprint",
+ },
+ user: { id: this.currentUserId },
+ achievement: { type: "earned", name: VaultItems_10_Added_Achievement.name as AchievementId },
+ };
+
+ this.eventStore.addEvent(earnedAchievement);
+ }
+}
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 68900b2ed74..2e8cf93e1ce 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -10620,5 +10620,8 @@
},
"cannotCreateCollection": {
"message": "Free organizations may have up to 2 collections. Upgrade to a paid plan to add more collections."
+ },
+ "achievements": {
+ "message": "Achievements"
}
}
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index d31cd0c84e2..9327d9df859 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -245,6 +245,10 @@ import { AuditService } from "@bitwarden/common/services/audit.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
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 {
PasswordStrengthService,
PasswordStrengthServiceAbstraction,
@@ -329,6 +333,8 @@ import { NoopViewCacheService } from "../platform/services/noop-view-cache.servi
import { AngularThemingService } from "../platform/services/theming/angular-theming.service";
import { AbstractThemingService } from "../platform/services/theming/theming.service.abstraction";
import { safeProvider, SafeProvider } from "../platform/utils/safe-provider";
+import { AchievementNotifierService as AchievementNotifierServiceAbstraction } from "../tools/achievements/achievement-notifier.abstraction";
+import { AchievementNotifierService } from "../tools/achievements/achievement-notifier.service";
import {
LOCALES_DIRECTORY,
@@ -1482,6 +1488,27 @@ const safeProviders: SafeProvider[] = [
ToastService,
],
}),
+ safeProvider({
+ provide: EventStoreAbstraction,
+ useClass: EventStore,
+ deps: [],
+ }),
+ safeProvider({
+ provide: AchievementServiceAbstraction,
+ useClass: DefaultAchievementService,
+ deps: [EventStoreAbstraction],
+ }),
+ safeProvider({
+ provide: AchievementNotifierServiceAbstraction,
+ useClass: AchievementNotifierService,
+ deps: [
+ AccountServiceAbstraction,
+ AchievementServiceAbstraction,
+ PlatformUtilsServiceAbstraction,
+ I18nServiceAbstraction,
+ ToastService,
+ ],
+ }),
];
@NgModule({
diff --git a/libs/angular/src/tools/achievements/achievement-notifier.abstraction.ts b/libs/angular/src/tools/achievements/achievement-notifier.abstraction.ts
new file mode 100644
index 00000000000..55daf4412c6
--- /dev/null
+++ b/libs/angular/src/tools/achievements/achievement-notifier.abstraction.ts
@@ -0,0 +1,3 @@
+export abstract class AchievementNotifierService {
+ abstract init(): Promise;
+}
diff --git a/libs/angular/src/tools/achievements/achievement-notifier.service.ts b/libs/angular/src/tools/achievements/achievement-notifier.service.ts
new file mode 100644
index 00000000000..652d3614f6e
--- /dev/null
+++ b/libs/angular/src/tools/achievements/achievement-notifier.service.ts
@@ -0,0 +1,56 @@
+import { firstValueFrom, switchMap, tap } from "rxjs";
+
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { AchievementService } from "@bitwarden/common/tools/achievements/achievement.service.abstraction";
+import { ToastService } from "@bitwarden/components";
+
+import { AchievementNotifierService as AchievementNotifierServiceAbstraction } from "./achievement-notifier.abstraction";
+
+export class AchievementNotifierService implements AchievementNotifierServiceAbstraction {
+ constructor(
+ private accountService: AccountService,
+ private achievementService: AchievementService,
+ private platformUtilsService: PlatformUtilsService,
+ private i18nService: I18nService,
+ private toastService: ToastService,
+ ) {}
+
+ async init() {
+ await this.setupListeners();
+ }
+
+ private async setupListeners() {
+ // FIXME Implement achievements earned filter and notififer
+ /* Get the userId from the accountService
+ * Subscribe to achievementService.achievementsEarned$(userId)
+ * Retrieve current device and filter out messages that are not for this client/device (achievements should be only shown on the device that earned them)
+ * Retrieve Achievement by AchievementId via the achievementService
+ * Use information from Achievement to fill out the options for the notification (toast)
+ * Invoke showing toast
+ */
+ // FIXME getClientType browswer and achievementEarned.service.name.extension won't match
+ 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,
+ });
+ });
+
+ // FIXME Migrate to use achievementHub.earned$() instead of achievementService.achievementsEarned$
+ }
+}
diff --git a/libs/common/src/tools/achievements/achievement.service.abstraction.ts b/libs/common/src/tools/achievements/achievement.service.abstraction.ts
new file mode 100644
index 00000000000..f1e67ea3a01
--- /dev/null
+++ b/libs/common/src/tools/achievements/achievement.service.abstraction.ts
@@ -0,0 +1,12 @@
+import { Observable } from "rxjs";
+
+import { UserId } from "@bitwarden/common/types/guid";
+
+import { Achievement, AchievementEarnedEvent, AchievementProgressEvent } from "./types";
+
+export abstract class AchievementService {
+ abstract achievementById$: (achievementId: string) => Observable;
+
+ abstract achievementsEarned$: (userId: UserId) => Observable;
+ abstract achievementsInProgress$: (userId: UserId) => Observable;
+}
diff --git a/libs/common/src/tools/achievements/default-achievement.service.ts b/libs/common/src/tools/achievements/default-achievement.service.ts
new file mode 100644
index 00000000000..c4e25bce87a
--- /dev/null
+++ b/libs/common/src/tools/achievements/default-achievement.service.ts
@@ -0,0 +1,60 @@
+import { filter, find, from, Observable } from "rxjs";
+
+import { UserId } from "@bitwarden/common/types/guid";
+
+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 DefaultAchievementService implements AchievementServiceAbstraction {
+ private _achievements: Achievement[] = [
+ VaultItems_1_Added_Achievement,
+ VaultItems_10_Added_Achievement,
+ ];
+
+ private _achievementsSubject = from(this._achievements);
+
+ achievementById$: (achievementId: string) => Observable;
+
+ // Provided by the AchievementHub
+ achievementsEarned$: (userId: UserId) => Observable;
+
+ // Provided by the AchievementHub
+ achievementsInProgress$: (userId: UserId) => Observable;
+
+ constructor(protected eventStore: EventStoreAbstraction) {
+ this.achievementById$ = (achievementId: AchievementId) =>
+ this._achievementsSubject.pipe(find((item: Achievement) => item.name === achievementId));
+
+ this.achievementsEarned$ = (userId: UserId) => {
+ return this.eventStore.events$.pipe(
+ filter(
+ (event): event is AchievementEarnedEvent =>
+ isEarnedEvent(event as AchievementEvent) && event.user.id === userId,
+ ),
+ );
+ };
+
+ this.achievementsInProgress$ = (userId: UserId) => {
+ return this.eventStore.events$.pipe(
+ filter(
+ (event): event is AchievementProgressEvent =>
+ isProgressEvent(event as AchievementEvent) && event.user.id === userId,
+ ),
+ );
+ };
+ }
+}
diff --git a/libs/common/src/tools/achievements/event-store.abstraction.service.ts b/libs/common/src/tools/achievements/event-store.abstraction.service.ts
new file mode 100644
index 00000000000..03252f390f0
--- /dev/null
+++ b/libs/common/src/tools/achievements/event-store.abstraction.service.ts
@@ -0,0 +1,10 @@
+import { Observable } from "rxjs";
+
+import { AchievementEarnedEvent, AchievementProgressEvent, UserActionEvent } from "./types";
+
+export abstract class EventStoreAbstraction {
+ abstract events$: Observable;
+ abstract addEvent(
+ event: UserActionEvent | AchievementProgressEvent | AchievementEarnedEvent,
+ ): boolean;
+}
diff --git a/libs/common/src/tools/achievements/event-store.ts b/libs/common/src/tools/achievements/event-store.ts
new file mode 100644
index 00000000000..5fcbf7b3a43
--- /dev/null
+++ b/libs/common/src/tools/achievements/event-store.ts
@@ -0,0 +1,22 @@
+import { Observable, Subject } from "rxjs";
+
+import { EventStoreAbstraction } from "./event-store.abstraction.service";
+import { AchievementEarnedEvent, AchievementProgressEvent } from "./types";
+
+// Will be replaced by the achievementHub
+export class EventStore implements EventStoreAbstraction {
+ private _events = new Subject();
+
+ events$: Observable =
+ this._events.asObservable();
+
+ constructor() {}
+
+ addEvent(event: AchievementProgressEvent | AchievementEarnedEvent): boolean {
+ // FIXME Collapse existing of same metric/higher count AchievementProgressEvents
+ //eslint-disable-next-line no-console
+ console.log("EventStore.addEvent", event);
+ this._events.next(event);
+ return true;
+ }
+}
diff --git a/libs/common/src/tools/achievements/examples/achievements/index.ts b/libs/common/src/tools/achievements/examples/achievements/index.ts
new file mode 100644
index 00000000000..bd37842fc0c
--- /dev/null
+++ b/libs/common/src/tools/achievements/examples/achievements/index.ts
@@ -0,0 +1,2 @@
+export * from "./vault-item-added";
+export * from "./login-item-added";
diff --git a/libs/common/src/tools/achievements/examples/achievements/login-item-added.ts b/libs/common/src/tools/achievements/examples/achievements/login-item-added.ts
new file mode 100644
index 00000000000..8c63af8a890
--- /dev/null
+++ b/libs/common/src/tools/achievements/examples/achievements/login-item-added.ts
@@ -0,0 +1,45 @@
+import { Achievement, AchievementId } from "../../types";
+import { VaultItemCreatedProgress } from "../metrics/metrics";
+
+const LoginItems_1_Added_Achievement: Achievement = {
+ achievement: "login-item-added" as AchievementId,
+ name: "Access granted",
+ description: "Saved your first login item with Bitwarden",
+ validator: "Threshold",
+ active: { metric: VaultItemCreatedProgress, high: 1 },
+ hidden: false,
+};
+
+const LoginItems_10_Added_Achievement: Achievement = {
+ achievement: "10-login-items-added" as AchievementId,
+ name: "10 Login Items Added",
+ description: "Add 10 item of type login to your vault",
+ validator: "Threshold",
+ active: { metric: VaultItemCreatedProgress, high: 10 },
+ hidden: false,
+};
+
+const LoginItems_50_Added_Achievement: Achievement = {
+ achievement: "50-login-items-added" as AchievementId,
+ name: "50 Login Items Added",
+ description: "Add 50 item of type login to your vault",
+ validator: "Threshold",
+ active: { metric: VaultItemCreatedProgress, high: 50 },
+ hidden: false,
+};
+
+const LoginItems_100_Added_Achievement: Achievement = {
+ achievement: "100-login-items-added" as AchievementId,
+ name: "100 Login Items Added",
+ description: "Add 100 item of type login to your vault",
+ validator: "Threshold",
+ active: { metric: VaultItemCreatedProgress, high: 100 },
+ hidden: false,
+};
+
+export {
+ LoginItems_1_Added_Achievement,
+ LoginItems_10_Added_Achievement,
+ LoginItems_50_Added_Achievement,
+ LoginItems_100_Added_Achievement,
+};
diff --git a/libs/common/src/tools/achievements/examples/achievements/vault-item-added.ts b/libs/common/src/tools/achievements/examples/achievements/vault-item-added.ts
new file mode 100644
index 00000000000..ff806ec7b91
--- /dev/null
+++ b/libs/common/src/tools/achievements/examples/achievements/vault-item-added.ts
@@ -0,0 +1,45 @@
+import { Achievement, AchievementId } from "../../types";
+import { VaultItemCreatedProgress } from "../metrics/metrics";
+
+const VaultItems_1_Added_Achievement: Achievement = {
+ achievement: "vault-item-added" as AchievementId,
+ name: "The chosen one",
+ description: "Saved your fist item to Bitwarden",
+ validator: "Threshold",
+ active: { metric: VaultItemCreatedProgress, high: 1 },
+ hidden: false,
+};
+
+const VaultItems_10_Added_Achievement: Achievement = {
+ achievement: "10-vault-items-added" as AchievementId,
+ name: "A decade of security",
+ description: "Saved your 10th item to Bitwarden",
+ validator: "Threshold",
+ active: { metric: VaultItemCreatedProgress, high: 10 },
+ hidden: false,
+};
+
+const VaultItems_50_Added_Achievement: Achievement = {
+ achievement: "50-vault-items-added" as AchievementId,
+ name: "It's 50/50 Vault Items Added",
+ description: "Saved your 50th item to Bitwarden",
+ validator: "Threshold",
+ active: { metric: VaultItemCreatedProgress, high: 50 },
+ hidden: false,
+};
+
+const VaultItems_100_Added_Achievement: Achievement = {
+ achievement: "100-vault-items-added" as AchievementId,
+ name: "Century mark, Now you are thinking with ciphers",
+ description: "Saved your 100th item to Bitwarden",
+ validator: "Threshold",
+ active: { metric: VaultItemCreatedProgress, high: 100 },
+ hidden: false,
+};
+
+export {
+ VaultItems_1_Added_Achievement,
+ VaultItems_10_Added_Achievement,
+ VaultItems_50_Added_Achievement,
+ VaultItems_100_Added_Achievement,
+};
diff --git a/libs/common/src/tools/achievements/examples/metrics/metrics.ts b/libs/common/src/tools/achievements/examples/metrics/metrics.ts
new file mode 100644
index 00000000000..8805cbf30b6
--- /dev/null
+++ b/libs/common/src/tools/achievements/examples/metrics/metrics.ts
@@ -0,0 +1,5 @@
+import { MetricId } from "../../types";
+
+const VaultItemCreatedProgress = "vault-item-quantity" as MetricId;
+
+export { VaultItemCreatedProgress };