diff --git a/.gitignore b/.gitignore index 2189378db..62d16dcf9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ node_modules # env files (can opt-in for committing if needed) .env* !.env.example + +# Firebase Admin SDK 서비스 계정 키 (절대 커밋 금지) +*firebase-adminsdk*.json diff --git a/backend/.env.example b/backend/.env.example index 8046dffa8..846d33be0 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -62,3 +62,8 @@ AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED # B2는 WHEN_REQUIRED로 설정 ADMIN_KEY="random string for admin notice" # backend/.env.docker.example # be, fe를 도커 컨테이너로 띄울 때는 서비스명으로 설정합니다. + +# ================= +# Firebase Admin SDK (FCM 푸시 알림 발송) +# ================= +FIREBASE_SERVICE_ACCOUNT_PATH=./firebase-adminsdk.json diff --git a/backend/migrations/1783344174180-CreateFcmToken-migration.ts b/backend/migrations/1783344174180-CreateFcmToken-migration.ts new file mode 100644 index 000000000..c197cdab8 --- /dev/null +++ b/backend/migrations/1783344174180-CreateFcmToken-migration.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateFcmTokenMigration1783344174180 implements MigrationInterface { + name = 'CreateFcmTokenMigration1783344174180'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "fcm_tokens" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "user_id" uuid NOT NULL, + "token" text NOT NULL, + "platform" character varying(10) NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_fcm_tokens" PRIMARY KEY ("id") + )`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_fcm_tokens_user_id_platform" ON "fcm_tokens" ("user_id", "platform")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX "public"."IDX_fcm_tokens_user_id_platform"`, + ); + await queryRunner.query(`DROP TABLE "fcm_tokens"`); + } +} diff --git a/backend/migrations/1783348447838-AddFcmTokenUniqueConstraint-migration.ts b/backend/migrations/1783348447838-AddFcmTokenUniqueConstraint-migration.ts new file mode 100644 index 000000000..958922226 --- /dev/null +++ b/backend/migrations/1783348447838-AddFcmTokenUniqueConstraint-migration.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddFcmTokenUniqueConstraint1783348447838 implements MigrationInterface { + name = 'AddFcmTokenUniqueConstraint1783348447838'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX IF EXISTS "public"."IDX_fcm_tokens_user_id_platform"`, + ); + await queryRunner.query( + `ALTER TABLE "fcm_tokens" ADD CONSTRAINT "UQ_fcm_tokens_user_id_platform" UNIQUE ("user_id", "platform")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "fcm_tokens" DROP CONSTRAINT "UQ_fcm_tokens_user_id_platform"`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_fcm_tokens_user_id_platform" ON "fcm_tokens" ("user_id", "platform")`, + ); + } +} diff --git a/backend/migrations/1783398050652-AddGroupMemberNotificationMuted-migration.ts b/backend/migrations/1783398050652-AddGroupMemberNotificationMuted-migration.ts new file mode 100644 index 000000000..7d7d0476b --- /dev/null +++ b/backend/migrations/1783398050652-AddGroupMemberNotificationMuted-migration.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddGroupMemberNotificationMuted1783398050652 implements MigrationInterface { + name = 'AddGroupMemberNotificationMuted1783398050652'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "group_members" ADD COLUMN IF NOT EXISTS "notification_muted" BOOLEAN NOT NULL DEFAULT FALSE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "group_members" DROP COLUMN IF EXISTS "notification_muted"`, + ); + } +} diff --git a/backend/package.json b/backend/package.json index 938a79a53..a97fdbc4b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -52,6 +52,7 @@ "class-validator": "^0.14.3", "cookie-parser": "^1.4.7", "dotenv": "^17.2.3", + "firebase-admin": "^14.1.0", "logform": "^2.7.0", "luxon": "^3.7.2", "nest-winston": "^1.10.2", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 6e29d8a72..2f29052f3 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -21,6 +21,7 @@ import { MediaModule } from './modules/media/media.module'; import { TrashModule } from './modules/trash/trash.module'; import { AnnouncementModule } from './modules/announcement/announcement.module'; import { InquiryModule } from './modules/inquiry/inquiry.module'; +import { NotificationModule } from './modules/notification/notification.module'; @Module({ imports: [ @@ -51,6 +52,7 @@ import { InquiryModule } from './modules/inquiry/inquiry.module'; TrashModule, AnnouncementModule, InquiryModule, + NotificationModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend/src/modules/group/controller/group-management.controller.ts b/backend/src/modules/group/controller/group-management.controller.ts index ff74cb9c7..47408c39a 100644 --- a/backend/src/modules/group/controller/group-management.controller.ts +++ b/backend/src/modules/group/controller/group-management.controller.ts @@ -189,6 +189,38 @@ export class GroupManagementController { return this.groupManagementService.getGroupPermission(user.sub, groupId); } + @Patch(':groupId/members/me/read') + @ApiOperation({ + summary: '그룹 읽음 처리', + description: '그룹 페이지 진입 시 lastReadAt을 현재 시간으로 갱신합니다.', + }) + @ApiParam({ name: 'groupId', description: '그룹 ID' }) + async markGroupAsRead( + @User() user: MyJwtPayload, + @Param('groupId') groupId: string, + ): Promise { + await this.groupManagementService.markGroupAsRead(user.sub, groupId); + } + + @Patch(':groupId/members/me/notification') + @ApiOperation({ + summary: '그룹 알림 설정 토글', + description: + '권한에 관계없이 모든 멤버가 해당 그룹의 알림을 켜고 끌 수 있습니다.', + }) + @ApiParam({ name: 'groupId', description: '그룹 ID' }) + async toggleGroupNotification( + @User() user: MyJwtPayload, + @Param('groupId') groupId: string, + @Body('muted') muted: boolean, + ): Promise { + await this.groupManagementService.toggleGroupNotification( + user.sub, + groupId, + muted, + ); + } + @UseGuards(GroupRoleGuard) @GroupRoles(GroupRoleEnum.VIEWER) @Patch(':groupId/members/me') diff --git a/backend/src/modules/group/dto/get-group-permission.dto.ts b/backend/src/modules/group/dto/get-group-permission.dto.ts index 25cbcd10c..0cda37b02 100644 --- a/backend/src/modules/group/dto/get-group-permission.dto.ts +++ b/backend/src/modules/group/dto/get-group-permission.dto.ts @@ -3,5 +3,8 @@ import { GroupRoleEnum } from '@/enums/group-role.enum'; export class GetGroupPermissionResponseDto { @ApiProperty({ enum: GroupRoleEnum }) - role: GroupRoleEnum; + role!: GroupRoleEnum; + + @ApiProperty() + notificationMuted!: boolean; } diff --git a/backend/src/modules/group/dto/get-groups.dto.ts b/backend/src/modules/group/dto/get-groups.dto.ts index a6c5f0512..40797618c 100644 --- a/backend/src/modules/group/dto/get-groups.dto.ts +++ b/backend/src/modules/group/dto/get-groups.dto.ts @@ -64,6 +64,12 @@ export class GroupItemDto { @ApiProperty({ description: '내 권한', enum: GroupRoleEnum, nullable: true }) permission: GroupRoleEnum | null; + + @ApiProperty({ description: '알림 음소거 여부' }) + notificationMuted: boolean; + + @ApiProperty({ description: '읽지 않은 활동 여부' }) + hasUnread: boolean; } export class GetGroupsResponseDto { diff --git a/backend/src/modules/group/entity/group_member.entity.ts b/backend/src/modules/group/entity/group_member.entity.ts index 67a118ca4..3248dfdbd 100644 --- a/backend/src/modules/group/entity/group_member.entity.ts +++ b/backend/src/modules/group/entity/group_member.entity.ts @@ -65,6 +65,9 @@ export class GroupMember { @JoinColumn({ name: 'profile_media_id' }) profileMedia?: MediaAsset | null; + @Column({ name: 'notification_muted', type: 'boolean', default: false }) + notificationMuted: boolean; + @Column({ name: 'last_read_at', type: 'timestamptz', nullable: true }) lastReadAt?: Date | null; diff --git a/backend/src/modules/group/group.module.ts b/backend/src/modules/group/group.module.ts index 64274b2fc..6ffc25cd3 100644 --- a/backend/src/modules/group/group.module.ts +++ b/backend/src/modules/group/group.module.ts @@ -30,6 +30,7 @@ import { GroupActivityActor } from './entity/group-activity-actor.entity'; import { AuthModule } from '../auth/auth.module'; import { PostModule } from '../post/post.module'; import { MediaModule } from '../media/media.module'; +import { NotificationModule } from '../notification/notification.module'; @Module({ imports: [ @@ -49,6 +50,7 @@ import { MediaModule } from '../media/media.module'; AuthModule, forwardRef(() => PostModule), MediaModule, + NotificationModule, ], providers: [ GroupService, diff --git a/backend/src/modules/group/service/group-activity.service.ts b/backend/src/modules/group/service/group-activity.service.ts index 753f89cdd..3666a5f81 100644 --- a/backend/src/modules/group/service/group-activity.service.ts +++ b/backend/src/modules/group/service/group-activity.service.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository, SelectQueryBuilder, Brackets } from 'typeorm'; +import { In, Not, Repository, SelectQueryBuilder, Brackets } from 'typeorm'; import { GroupActivityLog } from '../entity/group-activity-log.entity'; import { GroupActivityActor } from '../entity/group-activity-actor.entity'; @@ -11,6 +11,7 @@ import { GroupActivityItemDto, PaginatedGroupActivityResponseDto, } from '../dto/group-activity.dto'; +import { NotificationService } from '@/modules/notification/notification.service'; type RecordActivityInput = { groupId: string; @@ -20,8 +21,61 @@ type RecordActivityInput = { meta?: Record | null; }; +function buildActivityNotification( + actorName: string, + type: GroupActivityType, + meta?: Record | null, +): { title: string; body: string } | null { + const postTitle = ((meta?.title ?? meta?.afterTitle ?? '') as string) || ''; + const titlePart = postTitle ? ` "${postTitle}"` : ''; + + switch (type) { + case GroupActivityType.POST_CREATE: + return { + title: '새 기록', + body: `${actorName}님이 새 기록${titlePart}을(를) 작성했습니다.`, + }; + case GroupActivityType.POST_COLLAB_COMPLETE: + case GroupActivityType.POST_EDIT_COMPLETE: + case GroupActivityType.POST_UPDATE: + return { + title: '기록 수정', + body: `${actorName}님이${titlePart} 기록을 수정했습니다.`, + }; + case GroupActivityType.POST_DELETE: + return { + title: '기록 삭제', + body: `${actorName}님이${titlePart} 기록을 삭제했습니다.`, + }; + case GroupActivityType.MEMBER_JOIN: + return { + title: '새 멤버', + body: `${actorName}님이 그룹에 참여했습니다.`, + }; + case GroupActivityType.MEMBER_LEAVE: + return { + title: '멤버 탈퇴', + body: `${actorName}님이 그룹에서 나갔습니다.`, + }; + case GroupActivityType.MEMBER_REMOVE: + return { + title: '멤버 내보내기', + body: '그룹에서 멤버가 내보내졌습니다.', + }; + case GroupActivityType.GROUP_NAME_UPDATE: + return { + title: '그룹 이름 변경', + body: `그룹 이름이 "${meta?.afterName as string}"(으)로 변경되었습니다.`, + }; + default: + return null; + } +} + @Injectable() export class GroupActivityService { + private readonly logger = new Logger(GroupActivityService.name); + constructor( @InjectRepository(GroupActivityLog) private readonly logRepo: Repository, @@ -29,6 +83,7 @@ export class GroupActivityService { private readonly actorRepo: Repository, @InjectRepository(GroupMember) private readonly groupMemberRepo: Repository, + private readonly notificationService: NotificationService, ) {} async recordActivity(input: RecordActivityInput): Promise { @@ -58,6 +113,63 @@ export class GroupActivityService { await actorRepo.save(actors); } }); + + void this.dispatchNotification(input, actorIds).catch((e) => + this.logger.warn('푸시 알림 전송 실패', e), + ); + } + + private async dispatchNotification( + input: RecordActivityInput, + actorIds: string[], + ): Promise { + const actorMembers = + actorIds.length > 0 + ? await this.groupMemberRepo.find({ + where: { groupId: input.groupId, userId: In(actorIds) }, + relations: { user: true }, + }) + : []; + + const first = actorMembers[0]; + const baseName = first?.nicknameInGroup ?? first?.user?.nickname ?? '멤버'; + const extraCount = actorIds.length - 1; + const actorDisplayName = + extraCount > 0 ? `${baseName} 외 ${extraCount}명` : baseName; + + const notification = buildActivityNotification( + actorDisplayName, + input.type, + input.meta, + ); + if (!notification) return; + + const recipients = await this.groupMemberRepo.find({ + where: + actorIds.length > 0 + ? { + groupId: input.groupId, + userId: Not(In(actorIds)), + notificationMuted: false, + } + : { groupId: input.groupId, notificationMuted: false }, + select: ['userId'], + }); + + const recipientIds = recipients.map((m) => m.userId); + if (recipientIds.length === 0) return; + + const hasPost = input.refId && input.type !== GroupActivityType.POST_DELETE; + + await this.notificationService.sendToUsers( + recipientIds, + notification.title, + notification.body, + { + groupId: input.groupId, + ...(hasPost ? { postId: input.refId as string } : {}), + }, + ); } async getGroupActivities( diff --git a/backend/src/modules/group/service/group-management.service.ts b/backend/src/modules/group/service/group-management.service.ts index d9a2f26ba..fb3641657 100644 --- a/backend/src/modules/group/service/group-management.service.ts +++ b/backend/src/modules/group/service/group-management.service.ts @@ -598,9 +598,36 @@ export class GroupManagementService { groupId: string, ): Promise { const member = await this.groupService.ensureMember(userId, groupId, { - select: { role: true }, + select: { role: true, notificationMuted: true }, }); - return { role: member.role }; + return { + role: member.role, + notificationMuted: Boolean(member.notificationMuted), + }; + } + + async toggleGroupNotification( + userId: string, + groupId: string, + muted: boolean, + ): Promise { + await this.groupService.ensureMember(userId, groupId, { + select: { id: true }, + }); + await this.groupMemberRepo.update( + { userId, groupId }, + { notificationMuted: muted }, + ); + } + + async markGroupAsRead(userId: string, groupId: string): Promise { + await this.groupService.ensureMember(userId, groupId, { + select: { id: true }, + }); + await this.groupMemberRepo.update( + { userId, groupId }, + { lastReadAt: new Date() }, + ); } /** 그룹 내 내 설정 정보 수정 */ diff --git a/backend/src/modules/group/service/group.service.ts b/backend/src/modules/group/service/group.service.ts index 045a6feb9..980aa01d6 100644 --- a/backend/src/modules/group/service/group.service.ts +++ b/backend/src/modules/group/service/group.service.ts @@ -253,7 +253,12 @@ export class GroupService { const members = await this.groupMemberRepo .createQueryBuilder('gm') .innerJoin('gm.user', 'u') - .select(['gm.groupId', 'gm.role']) + .select([ + 'gm.groupId', + 'gm.role', + 'gm.notificationMuted', + 'gm.lastReadAt', + ]) .where('gm.userId = :userId', { userId }) .andWhere('u.deletedAt IS NULL') .getMany(); @@ -264,6 +269,12 @@ export class GroupService { const groupIds = members.map((m) => m.groupId); const roleByGroupId = new Map(members.map((m) => [m.groupId, m.role])); + const mutedByGroupId = new Map( + members.map((m) => [m.groupId, Boolean(m.notificationMuted)]), + ); + const lastReadAtByGroupId = new Map( + members.map((m) => [m.groupId, m.lastReadAt ?? null]), + ); if (groupIds.length > 0) { this.cleanupStaleGroupCovers(groupIds); } @@ -368,6 +379,12 @@ export class GroupService { } : null, permission: roleByGroupId.get(groupId) ?? null, + notificationMuted: mutedByGroupId.get(groupId) ?? false, + hasUnread: (() => { + const lastRead = lastReadAtByGroupId.get(groupId) ?? null; + if (!lastRead) return true; + return new Date(lastActivityAt) > new Date(lastRead); + })(), }); } diff --git a/backend/src/modules/notification/dto/register-fcm-token.dto.ts b/backend/src/modules/notification/dto/register-fcm-token.dto.ts new file mode 100644 index 000000000..0dd32e11b --- /dev/null +++ b/backend/src/modules/notification/dto/register-fcm-token.dto.ts @@ -0,0 +1,9 @@ +import { IsIn, IsString } from 'class-validator'; + +export class RegisterFcmTokenDto { + @IsString() + token!: string; + + @IsIn(['web', 'android']) + platform!: 'web' | 'android'; +} diff --git a/backend/src/modules/notification/entity/fcm-token.entity.ts b/backend/src/modules/notification/entity/fcm-token.entity.ts new file mode 100644 index 000000000..a98c4cb5a --- /dev/null +++ b/backend/src/modules/notification/entity/fcm-token.entity.ts @@ -0,0 +1,32 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Unique, +} from 'typeorm'; + +export type FcmPlatform = 'web' | 'android'; + +@Entity('fcm_tokens') +@Unique(['userId', 'platform']) +export class FcmToken { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'user_id', type: 'uuid' }) + userId: string; + + @Column({ name: 'token', type: 'text' }) + token: string; + + @Column({ name: 'platform', type: 'varchar', length: 10 }) + platform: FcmPlatform; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} diff --git a/backend/src/modules/notification/notification.controller.ts b/backend/src/modules/notification/notification.controller.ts new file mode 100644 index 000000000..c4c424b27 --- /dev/null +++ b/backend/src/modules/notification/notification.controller.ts @@ -0,0 +1,37 @@ +import { Body, Controller, Delete, Post, Req, UseGuards } from '@nestjs/common'; +import type { Request } from 'express'; +import { JwtAuthGuard } from '@/modules/auth/jwt/jwt.guard'; +import { NotificationService } from './notification.service'; +import { RegisterFcmTokenDto } from './dto/register-fcm-token.dto'; + +interface RequestWithUser extends Request { + user: { sub: string }; +} + +@UseGuards(JwtAuthGuard) +@Controller({ path: 'notifications', version: '1' }) +export class NotificationController { + constructor(private readonly notificationService: NotificationService) {} + + @Post('fcm-token') + async registerToken( + @Req() req: RequestWithUser, + @Body() dto: RegisterFcmTokenDto, + ) { + await this.notificationService.registerToken( + req.user.sub, + dto.token, + dto.platform, + ); + return { success: true }; + } + + @Delete('fcm-token') + async removeToken( + @Req() req: RequestWithUser, + @Body() dto: Pick, + ) { + await this.notificationService.removeToken(req.user.sub, dto.platform); + return { success: true }; + } +} diff --git a/backend/src/modules/notification/notification.module.ts b/backend/src/modules/notification/notification.module.ts new file mode 100644 index 000000000..29dc9e5d7 --- /dev/null +++ b/backend/src/modules/notification/notification.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FcmToken } from './entity/fcm-token.entity'; +import { NotificationService } from './notification.service'; +import { NotificationController } from './notification.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([FcmToken])], + controllers: [NotificationController], + providers: [NotificationService], + exports: [NotificationService], +}) +export class NotificationModule {} diff --git a/backend/src/modules/notification/notification.service.ts b/backend/src/modules/notification/notification.service.ts new file mode 100644 index 000000000..2f632480e --- /dev/null +++ b/backend/src/modules/notification/notification.service.ts @@ -0,0 +1,130 @@ +import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { + initializeApp, + cert, + type App, + ServiceAccount, +} from 'firebase-admin/app'; +import { getMessaging } from 'firebase-admin/messaging'; +import { resolve } from 'path'; +import { FcmToken, type FcmPlatform } from './entity/fcm-token.entity'; + +@Injectable() +export class NotificationService implements OnModuleInit { + private readonly logger = new Logger(NotificationService.name); + private app: App | null = null; + + constructor( + private readonly configService: ConfigService, + @InjectRepository(FcmToken) + private readonly fcmTokenRepo: Repository, + ) {} + + onModuleInit() { + const serviceAccountPath = this.configService.get( + 'FIREBASE_SERVICE_ACCOUNT_PATH', + ); + if (!serviceAccountPath) { + this.logger.warn( + 'FIREBASE_SERVICE_ACCOUNT_PATH가 설정되지 않아 FCM을 비활성화합니다.', + ); + return; + } + + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const serviceAccount = require( + resolve(serviceAccountPath), + ) as ServiceAccount; + this.app = initializeApp({ credential: cert(serviceAccount) }); + this.logger.log('Firebase Admin SDK 초기화 완료'); + } catch (e) { + this.logger.error('Firebase Admin SDK 초기화 실패', e); + } + } + + async registerToken( + userId: string, + token: string, + platform: FcmPlatform, + ): Promise { + await this.fcmTokenRepo.upsert( + { userId, token, platform }, + { conflictPaths: ['userId', 'platform'] }, + ); + } + + async removeToken(userId: string, platform: FcmPlatform): Promise { + await this.fcmTokenRepo.delete({ userId, platform }); + } + + async sendToUsers( + userIds: string[], + title: string, + body: string, + data?: Record, + ): Promise { + if (!this.app || userIds.length === 0) return; + + const tokens = await this.fcmTokenRepo.find({ + where: userIds.map((userId) => ({ userId })), + select: ['token', 'platform'], + }); + + if (tokens.length === 0) return; + + try { + // 웹: data-only 메시지 → Firebase가 자동 표시하지 않고 SW onBackgroundMessage에서 처리. + // notification 필드가 있으면 Firebase가 자동 표시(FCM_MSG 래핑)와 onBackgroundMessage 둘 다 호출해 + // 알림이 중복되고 notificationclick에서 data 파싱이 깨지는 문제가 발생함. + // 안드로이드: notification 필드 필요 (네이티브 FCM이 시스템 알림 표시에 사용). + // 웹: FidMessage({ fid }) — DB의 token 컬럼에 FID가 저장됨 + // 안드로이드: TokenMessage({ token }) — DB의 token 컬럼에 FCM 등록 토큰이 저장됨 + const identifiers = tokens.map(({ token }) => token); + const messages = tokens.map(({ token, platform }) => + platform === 'web' + ? { + fid: token, + data: { title, body, ...(data ?? {}) }, + webpush: { headers: { Urgency: 'high' } }, + } + : { + token, + notification: { title, body }, + ...(data ? { data } : {}), + webpush: { notification: { icon: '/web-app-icon-192x192.png' } }, + }, + ); + const response = await getMessaging(this.app).sendEach(messages); + + const staleIdentifiers = response.responses + .map((r, i) => ({ ...r, identifier: identifiers[i] })) + .filter( + (r) => + !r.success && + (r.error?.code === 'messaging/registration-token-not-registered' || + r.error?.code === 'messaging/invalid-argument'), + ) + .map((r) => r.identifier); + + if (staleIdentifiers.length > 0) { + await this.fcmTokenRepo + .createQueryBuilder() + .delete() + .where('token IN (:...tokens)', { tokens: staleIdentifiers }) + .execute(); + this.logger.log(`만료 FCM 식별자 ${staleIdentifiers.length}개 삭제`); + } + + const failedCount = response.responses.filter((r) => !r.success).length; + if (failedCount > 0) { + this.logger.warn(`FCM 발송 실패: ${failedCount}/${messages.length}`); + } + } catch (e) { + this.logger.error('FCM 발송 오류', e); + } + } +} diff --git a/frontend/.env.example b/frontend/.env.example index 608d4a681..66dfe4ff4 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -12,3 +12,14 @@ AUTH_URL= AUTH_SECRET= NEXT_PUBLIC_IMAGE_DOMAIN=http://localhost:3000 NEXT_PUBLIC_CLIENT_URL=http://localhost:3000 + +# ================= +# Firebase (FCM 푸시 알림) +# ================= +NEXT_PUBLIC_FIREBASE_API_KEY= +NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN= +NEXT_PUBLIC_FIREBASE_PROJECT_ID= +NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET= +NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID= +NEXT_PUBLIC_FIREBASE_APP_ID= +NEXT_PUBLIC_FIREBASE_VAPID_KEY= diff --git a/frontend/package.json b/frontend/package.json index f3bded732..c4f4cba85 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "@capacitor/clipboard": "^7.0.4", "@capacitor/core": "^7.0.0", "@capacitor/network": "^8.0.1", + "@capacitor/push-notifications": "^8.1.1", "@capacitor/share": "^7.0.4", "@capacitor/splash-screen": "^7.0.5", "@capacitor/status-bar": "^7.0.6", @@ -32,10 +33,12 @@ "@tanstack/react-query": "^5.90.12", "@types/js-cookie": "^3.0.6", "@vis.gl/react-google-maps": "^1.7.1", + "capacitor-native-settings": "^8.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", "exifr": "^7.1.3", + "firebase": "^12.15.0", "framer-motion": "^12.23.26", "import-in-the-middle": "2.0.6", "js-cookie": "^3.0.5", diff --git a/frontend/public/firebase-messaging-sw.js b/frontend/public/firebase-messaging-sw.js new file mode 100644 index 000000000..277a3b2f5 --- /dev/null +++ b/frontend/public/firebase-messaging-sw.js @@ -0,0 +1,60 @@ +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', (event) => + event.waitUntil(self.clients.claim()), +); + +// firebase.messaging()보다 먼저 등록해야 함. +// Firebase compat 내부 notificationclick 핸들러가 나중에 등록되므로 +// 우리 핸들러가 먼저 실행되어 user gesture context 안에서 openWindow를 호출할 수 있음. +// 반대 순서로 등록하면 Firebase 핸들러가 첫 번째 클릭 시 context를 소비해 +// 우리 openWindow가 no-op이 되는 문제가 발생함. +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + const { groupId, postId } = event.notification.data ?? {}; + const url = + postId && groupId + ? `/record/${postId}?scope=group&groupId=${groupId}` + : postId + ? `/record/${postId}` + : groupId + ? `/group/${groupId}` + : '/shared'; + + // clients.navigate()/postMessage()/BroadcastChannel은 모두 SW가 해당 클라이언트를 + // 제어할 때만 동작함. Firebase SW 스코프(/firebase-cloud-messaging-push-scope)는 + // 앱 페이지를 제어하지 않으므로, openWindow로 직접 URL 열기가 가장 확실한 방법. + // PWA: 기존 창 재사용 / 일반 브라우저: 새 탭으로 열림. + // 백그라운드 브라우저 → 포그라운드 전환도 notificationclick 사용자 제스처로 허용됨. + event.waitUntil(clients.openWindow(url)); +}); + +importScripts( + 'https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js', +); +importScripts( + 'https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js', +); + +firebase.initializeApp({ + apiKey: 'AIzaSyAaP2MD3vim6zP0OQYw4AFAfP2nWh5GlDk', + authDomain: 'friendly-magnet-481309-k6.firebaseapp.com', + projectId: 'friendly-magnet-481309-k6', + storageBucket: 'friendly-magnet-481309-k6.firebasestorage.app', + messagingSenderId: '513351224432', + appId: '1:513351224432:web:e1cd1b1b39ee73862bb8b5', +}); + +const messaging = firebase.messaging(); + +messaging.onBackgroundMessage((payload) => { + // 백엔드에서 웹 토큰에는 data-only 메시지를 보냄 (notification 필드 없음). + // Firebase 자동 표시 없이 여기서만 알림을 표시하므로 중복 없음. + // data에 title/body/postId/groupId 등이 모두 포함됨. + const title = payload.data?.title ?? '잇다 알림'; + const body = payload.data?.body ?? ''; + self.registration.showNotification(title, { + body, + icon: '/web-app-icon-192x192.png', + data: payload.data ?? {}, + }); +}); diff --git a/frontend/src/app/(main)/profile/_components/Setting.tsx b/frontend/src/app/(main)/profile/_components/Setting.tsx index dd0183f88..97d9fab3d 100644 --- a/frontend/src/app/(main)/profile/_components/Setting.tsx +++ b/frontend/src/app/(main)/profile/_components/Setting.tsx @@ -10,6 +10,7 @@ import { } from '@/components/ui/drawer'; import { AlertCircle, + Bell, ChevronRight, Download, FileText, @@ -33,6 +34,17 @@ import { useApiDelete, useApiPost } from '@/hooks/useApi'; import { toast } from 'sonner'; import { signOut } from 'next-auth/react'; import * as Sentry from '@sentry/nextjs'; +import { removeFcmToken } from '@/lib/api/notification'; +import { + registerAndroidToken, + requestAndRegisterWebToken, +} from '@/hooks/usePushNotification'; + +const isNativePlatform = () => + typeof window !== 'undefined' && + !!( + window as unknown as { Capacitor?: { isNativePlatform?: () => boolean } } + ).Capacitor?.isNativePlatform?.(); export default function Setting() { const router = useRouter(); @@ -42,6 +54,8 @@ export default function Setting() { const { isInstalled, promptInstall, isIOS, isSafari, isMacOS } = usePWAInstall(); const [showInstructions, setShowInstructions] = useState(false); + const [pushEnabled, setPushEnabled] = useState(false); + const [pushBlocked, setPushBlocked] = useState(false); const { mutate: logout } = useApiPost('/api/auth/logout', { onSuccess: async () => { @@ -85,6 +99,27 @@ export default function Setting() { return () => cancelAnimationFrame(raId); }, []); + useEffect(() => { + (async () => { + if (isNativePlatform()) { + try { + const { PushNotifications } = + await import('@capacitor/push-notifications'); + const { receive } = await PushNotifications.checkPermissions(); + const disabled = + localStorage.getItem('push_notifications_disabled') === 'true'; + setPushEnabled(receive === 'granted' && !disabled); + setPushBlocked(receive === 'denied'); + } catch {} + } else if ('Notification' in window) { + const disabled = + localStorage.getItem('push_notifications_disabled') === 'true'; + setPushEnabled(Notification.permission === 'granted' && !disabled); + setPushBlocked(Notification.permission === 'denied'); + } + })(); + }, []); + // Hydration 불일치를 방지하기 위해 마운트되기 전에는 아무것도 렌더링하지 않음 if (!mounted) { return null; @@ -107,6 +142,53 @@ export default function Setting() { setTheme(nextTheme); }; + const handleTogglePush = async () => { + const platform = isNativePlatform() ? 'android' : 'web'; + if (pushEnabled) { + try { + await removeFcmToken(platform); + } catch {} + localStorage.setItem('push_notifications_disabled', 'true'); + setPushEnabled(false); + return; + } + + try { + if (isNativePlatform()) { + await registerAndroidToken(); + const { PushNotifications } = + await import('@capacitor/push-notifications'); + const { receive } = await PushNotifications.checkPermissions(); + if (receive === 'granted') { + localStorage.removeItem('push_notifications_disabled'); + setPushEnabled(true); + setPushBlocked(false); + } else { + setPushBlocked(true); + const { NativeSettings, AndroidSettings } = + await import('capacitor-native-settings'); + await NativeSettings.openAndroid({ + option: AndroidSettings.AppNotification, + }).catch(() => {}); + } + } else { + await requestAndRegisterWebToken(); + if (Notification.permission === 'granted') { + localStorage.removeItem('push_notifications_disabled'); + setPushEnabled(true); + setPushBlocked(false); + } else { + setPushBlocked(Notification.permission === 'denied'); + if (Notification.permission === 'denied') { + toast.info('설정에서 알림 권한을 허용해 주세요.'); + } + } + } + } catch { + toast.error('알림 설정 변경에 실패했습니다.'); + } + }; + const handleContact = () => { router.push('/inquiry'); }; @@ -169,6 +251,33 @@ export default function Setting() { + {('Notification' in window || isNativePlatform()) && ( +
+
+
+ +
+ + 푸시 알림 + +
+ +
+ )} +
+ - {/* 제목 영역 */} -
-
-
-
- - {/* 콘텐츠 블록들 */} -
-
-
-
-
-
-
-
-
- ); -} - -export default function Loading() { - return ; -} diff --git a/frontend/src/app/(post)/record/[recordId]/page.tsx b/frontend/src/app/(post)/record/[recordId]/page.tsx index b11495d27..80e997977 100644 --- a/frontend/src/app/(post)/record/[recordId]/page.tsx +++ b/frontend/src/app/(post)/record/[recordId]/page.tsx @@ -1,10 +1,12 @@ import type { Metadata } from 'next'; -import { getCachedRecordDetail } from '@/lib/api/records'; -import RecordDetailContent from '../_components/RecordDetailContent'; import { - ImageValue, - TextValue, -} from '@/lib/types/record'; + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; +import { getCachedRecordDetail, recordDetailOptions } from '@/lib/api/records'; +import RecordDetailContent from '../_components/RecordDetailContent'; +import { ImageValue, TextValue } from '@/lib/types/record'; import { get } from '@/lib/api/api'; import { SingleResolveResponse } from '@/hooks/useMediaResolve'; import { randomBaseImage } from '@/lib/image'; @@ -90,5 +92,19 @@ export async function generateMetadata({ export default async function RecordPage({ params }: RecordPageProps) { const { recordId } = await params; - return ; + const queryClient = new QueryClient(); + try { + // generateMetadata에서 getCachedRecordDetail을 이미 호출했다면 React cache()로 중복 요청 없이 재사용됨. + // setQueryData로 서버 데이터를 클라이언트 QueryClient에 이식해 useSuspenseQuery가 바로 캐시를 찾게 함. + const data = await getCachedRecordDetail(recordId); + queryClient.setQueryData(recordDetailOptions(recordId).queryKey, data); + } catch { + // 에러는 RecordDetail 클라이언트 컴포넌트의 ErrorBoundary에서 처리 + } + + return ( + + + + ); } diff --git a/frontend/src/app/(post)/record/_components/RecordDetail.tsx b/frontend/src/app/(post)/record/_components/RecordDetail.tsx index 8c9ef0bfb..0ba157bd2 100644 --- a/frontend/src/app/(post)/record/_components/RecordDetail.tsx +++ b/frontend/src/app/(post)/record/_components/RecordDetail.tsx @@ -3,20 +3,34 @@ import { recordDetailOptions } from '@/lib/api/records'; import { Block } from '@/lib/types/record'; import { useSuspenseQuery } from '@tanstack/react-query'; -import { useSearchParams } from 'next/navigation'; -import RecordDetailHeaderActions from './RecordDetailHeaderActions'; +import dynamic from 'next/dynamic'; +import Back from '@/components/Back'; import BlockContent from '@/components/BlockContent'; import { cn } from '@/lib/utils'; import AssetImage from '@/components/AssetImage'; +// SSR에서 렌더링하지 않음: Radix Popover의 useId가 서버-클라이언트 간 렌더 트리 순서 차이로 +// aria-controls 불일치(하이드레이션 에러)를 일으키기 때문. +// loading으로 Back + 우측 placeholder를 즉시 렌더링해 레이아웃 이동 방지. +const RecordDetailHeaderActions = dynamic( + () => import('./RecordDetailHeaderActions'), + { + ssr: false, + loading: () => ( + <> + +
+ + ), + }, +); + interface RecordDetailProps { recordId: string; } export default function RecordDetail({ recordId }: RecordDetailProps) { - const searchParams = useSearchParams(); - const scope = searchParams.get('scope') === 'group' ? 'group' : 'personal'; - const { data: record } = useSuspenseQuery(recordDetailOptions(recordId, scope)); + const { data: record } = useSuspenseQuery(recordDetailOptions(recordId)); // 블록을 row별로 그룹화 const rowMap = new Map(); diff --git a/frontend/src/app/(post)/shared/_components/SharedHeaderActions.tsx b/frontend/src/app/(post)/shared/_components/SharedHeaderActions.tsx index 82631455b..a45a189b7 100644 --- a/frontend/src/app/(post)/shared/_components/SharedHeaderActions.tsx +++ b/frontend/src/app/(post)/shared/_components/SharedHeaderActions.tsx @@ -59,6 +59,8 @@ export default function SharedHeaderActions() { lastActivityAt: response.data.createdAt, latestPost: null, permission: 'ADMIN', + notificationMuted: false, + hasUnread: false, }; // 서버 응답 데이터를 캐시에 즉시 추가 queryClient.setQueryData(['shared'], (old) => { diff --git a/frontend/src/app/(post)/shared/_components/SharedRecords.tsx b/frontend/src/app/(post)/shared/_components/SharedRecords.tsx index d01b703ed..d2b651204 100644 --- a/frontend/src/app/(post)/shared/_components/SharedRecords.tsx +++ b/frontend/src/app/(post)/shared/_components/SharedRecords.tsx @@ -58,7 +58,8 @@ const GroupCard = memo(function GroupCard({ count={count} latestTitle={group.latestPost?.title || '최신 기록이 없어요'} latestLocation={group.latestPost?.placeName} - hasNotification={false} + hasNotification={group.hasUnread} + notificationMuted={group.notificationMuted} cover={group.cover} onClick={handleClick} onChangeCover={isViewer ? undefined : handleChangeCover} diff --git a/frontend/src/app/AuthContext.tsx b/frontend/src/app/AuthContext.tsx index 1ad64f669..fae9d0bdf 100644 --- a/frontend/src/app/AuthContext.tsx +++ b/frontend/src/app/AuthContext.tsx @@ -11,6 +11,7 @@ import { useEffect } from 'react'; import { useTheme } from 'next-themes'; import { useAuthStore } from '@/store/useAuthStore'; import { invalidateSessionCache } from '@/lib/api/auth'; +import { usePushNotification } from '@/hooks/usePushNotification'; const isNativePlatform = () => typeof window !== 'undefined' && @@ -26,6 +27,8 @@ function SessionGuard({ children }: { children: React.ReactNode }) { const logout = useAuthStore((state) => state.logout); const { resolvedTheme } = useTheme(); + usePushNotification(userType !== null); + // skipHydration: true이므로 마운트 후 localStorage에서 상태 복원 useEffect(() => { useAuthStore.persist.rehydrate(); @@ -67,7 +70,10 @@ function SessionGuard({ children }: { children: React.ReactNode }) { const androidTheme = resolvedTheme === 'dark' ? 'dark' : 'light'; const androidBridge = ( window as unknown as { - AndroidBridge?: { themeChange: (t: string) => void; appReady: () => void }; + AndroidBridge?: { + themeChange: (t: string) => void; + appReady: () => void; + }; } ).AndroidBridge; diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 5fcf683f2..1fa62a148 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -16,6 +16,7 @@ import NativeStatusBarSync from '@/components/NativeStatusBarSync'; import NetworkGuard from '@/components/NetworkGuard'; import AndroidBackHandler from '@/components/AndroidBackHandler'; import ServiceGuard from '@/components/ServiceGuard'; +import AndroidNotificationHandler from '@/components/AndroidNotificationHandler'; import { GoogleAnalytics } from '@next/third-parties/google'; const notoSans = Noto_Sans_KR({ @@ -187,6 +188,8 @@ export default function RootLayout({ __html: ` if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch(function() {}); + navigator.serviceWorker.register('/firebase-messaging-sw.js', { scope: '/firebase-cloud-messaging-push-scope' }) + .catch(function() {}); } `, }} @@ -262,6 +265,7 @@ export default function RootLayout({ +
void) | null = null; + +function handleNotificationAction(data: Record | undefined) { + const postId = data?.postId; + const groupId = data?.groupId; + const route = + postId && groupId + ? `/record/${postId}?scope=group&groupId=${groupId}` + : postId + ? `/record/${postId}` + : groupId + ? `/group/${groupId}` + : null; + if (!route) return; + + if (navigateFn) { + navigateFn(route); + } else { + pendingRoute = route; + } +} + +if (typeof window !== 'undefined') { + const platform = ( + window as unknown as { Capacitor?: { getPlatform?: () => string } } + ).Capacitor?.getPlatform?.(); + + if (platform === 'android') { + import('@capacitor/push-notifications').then(({ PushNotifications }) => { + PushNotifications.addListener( + 'pushNotificationActionPerformed', + (action) => { + handleNotificationAction( + action.notification.data as Record | undefined, + ); + }, + ); + }); + } +} + +function extractPostId(route: string): string | null { + const match = route.match(/\/record\/([^?]+)/); + return match ? match[1] : null; +} + +export default function AndroidNotificationHandler() { + const router = useRouter(); + const userId = useAuthStore((s) => s.userId); + const queryClient = useQueryClient(); + + useEffect(() => { + // 인증된 상태에서만 라우팅 실행 + if (!userId) return; + + navigateFn = (route: string) => { + const postId = extractPostId(route); + const hasCache: boolean = postId + ? queryClient.getQueryData(recordDetailOptions(postId).queryKey) !== + undefined + : true; + + if (hasCache) { + // 캐시 존재 → 즉시 이동(stale 데이터 표시), prefetch로 백그라운드 갱신 + // useSuspenseQuery는 stale 데이터가 있어도 suspend하지 않음 → 스켈레톤 없음 + if (postId) + queryClient + .prefetchQuery(recordDetailOptions(postId)) + .catch(() => {}); + router.push(route); + } else { + // 캐시 없음 → prefetch 완료 후 이동 + // 서버 컴포넌트 fetch보다 데이터가 먼저 캐시에 들어와 스켈레톤 방지 + const prefetch = postId + ? queryClient + .prefetchQuery(recordDetailOptions(postId)) + .catch(() => {}) + : Promise.resolve(); + prefetch.finally(() => router.push(route)); + } + }; + + if (pendingRoute) { + const route = pendingRoute; + pendingRoute = null; + navigateFn(route); + } + + return () => { + navigateFn = null; + }; + }, [router, userId, queryClient]); + + return null; +} diff --git a/frontend/src/components/ui/RecordCard.tsx b/frontend/src/components/ui/RecordCard.tsx index 6479a90bb..74abd9033 100644 --- a/frontend/src/components/ui/RecordCard.tsx +++ b/frontend/src/components/ui/RecordCard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Clock, MapPin } from 'lucide-react'; +import { BellOff, Clock, MapPin } from 'lucide-react'; import { PostCard } from './PostCard'; import { useRouter } from 'next/navigation'; import { GroupCover } from '@/lib/types/group'; @@ -18,6 +18,7 @@ export interface RecordCardProps { latestLocation?: string | null; cover?: GroupCover | null; hasNotification?: boolean; + notificationMuted?: boolean; createdAt?: string; height?: string; onClick?: () => void; @@ -33,6 +34,7 @@ export const RecordCard = memo(function RecordCard({ latestLocation, cover, hasNotification, + notificationMuted, onClick, onChangeCover, createdAt, @@ -47,6 +49,13 @@ export const RecordCard = memo(function RecordCard({ onClick={onClick} priorityLoad={priorityLoad} > + {/* 알림 뮤트 표시 */} + {notificationMuted && ( +
+ +
+ )} + {/* 커버 변경 버튼 */} {onChangeCover && ( + typeof window !== 'undefined' && + !!( + window as unknown as { Capacitor?: { isNativePlatform?: () => boolean } } + ).Capacitor?.isNativePlatform?.(); + +export async function registerAndroidToken() { + const { PushNotifications } = await import('@capacitor/push-notifications'); + + const result = await PushNotifications.requestPermissions(); + if (result.receive !== 'granted') return; + + await new Promise((resolve) => { + PushNotifications.addListener('registration', async ({ value: token }) => { + await registerFcmToken(token, 'android').catch(() => {}); + await PushNotifications.removeAllListeners(); + resolve(); + }); + PushNotifications.addListener('registrationError', async () => { + await PushNotifications.removeAllListeners(); + resolve(); + }); + PushNotifications.register(); + }); +} + +// onRegistered 콜백을 await 가능한 형태로 변환 +function registerAndGetFid( + messaging: Messaging, + options: { + vapidKey: string | undefined; + serviceWorkerRegistration: ServiceWorkerRegistration; + }, +): Promise { + return new Promise((resolve) => { + const unsubOnRegistered = onRegistered(messaging, (fid) => { + unsubOnRegistered(); + resolve(fid); + }); + fcmRegister(messaging, options).catch(() => { + unsubOnRegistered(); + resolve(null); + }); + }); +} + +async function getAndRegisterWebFcmToken() { + const messaging = await getFirebaseMessaging(); + if (!messaging) return; + + const swReg = await navigator.serviceWorker.register( + '/firebase-messaging-sw.js', + { scope: '/firebase-cloud-messaging-push-scope' }, + ); + + if (!swReg.active) { + await new Promise((resolve) => { + const sw = swReg.installing ?? swReg.waiting; + if (!sw) { + resolve(); + return; + } + const handler = () => { + if (sw.state === 'activated') { + sw.removeEventListener('statechange', handler); + resolve(); + } + }; + sw.addEventListener('statechange', handler); + if (sw.state === 'activated' || swReg.active) { + sw.removeEventListener('statechange', handler); + resolve(); + } + }); + } + + const options = { vapidKey: VAPID_KEY, serviceWorkerRegistration: swReg }; + + const existingSub = await swReg.pushManager + .getSubscription() + .catch(() => null); + + if (!existingSub) { + // SW unregister로 push subscription이 소멸한 경우: + // 기존 FID 등록 정보를 삭제해 fcmRegister()가 새 push subscription으로 재등록하도록 강제 + await unregister(messaging).catch(() => {}); + } + + const fid = await registerAndGetFid(messaging, options); + if (fid) { + await registerFcmToken(fid, 'web').catch(() => {}); + } +} + +// 로그인 후 자동 갱신용 — 이미 허용된 경우에만 조용히 토큰 등록 (다이얼로그 없음) +async function registerWebToken() { + if (!('Notification' in window)) return; + if (Notification.permission !== 'granted') return; + await getAndRegisterWebFcmToken(); +} + +// Settings 토글 클릭 시 — 브라우저 권한 다이얼로그 표시 후 토큰 등록 +export async function requestAndRegisterWebToken() { + if (!('Notification' in window)) return; + + const permission = await Notification.requestPermission(); + if (permission !== 'granted') return; + + await getAndRegisterWebFcmToken(); +} + +export function usePushNotification(enabled: boolean) { + useEffect(() => { + if (!enabled) return; + if (typeof window === 'undefined') return; + + const register = isNativePlatform() + ? registerAndroidToken + : registerWebToken; + register().catch(() => {}); + }, [enabled]); +} diff --git a/frontend/src/lib/api/api.ts b/frontend/src/lib/api/api.ts index 0b0032bef..a7b3af398 100644 --- a/frontend/src/lib/api/api.ts +++ b/frontend/src/lib/api/api.ts @@ -2,7 +2,7 @@ import { ApiResponse } from '../types/response'; import { getAccessToken, refreshAccessToken, handleLogout } from './auth'; import * as Sentry from '@sentry/nextjs'; import { useAuthStore } from '@/store/useAuthStore'; -import { getBackendApiBaseUrl } from '@/lib/config/backend'; +import { getBackendOrigin } from '@/lib/config/backend'; import { isRefreshableAuthError, isTerminalAuthError, @@ -23,8 +23,9 @@ function getApiBaseUrl() { return ''; } - // 서버 환경 - 백엔드 절대 URL - return getBackendApiBaseUrl(); + // 서버 환경 - /api → /v1 치환이 fetchApi 내부에서 이루어지므로 + // /v1을 포함하지 않는 origin만 반환해야 이중 /v1이 생기지 않음 + return getBackendOrigin(); } interface FetchOptions extends RequestInit { @@ -325,15 +326,14 @@ export async function fetchApi( } = options; const currentBaseUrl = getApiBaseUrl(); - // 서버 환경에서는 /api 접두사 제거 (base URL에 이미 /v1 포함) + // 서버 환경에서는 /api → /v1 치환 (Next.js rewrite 없이 백엔드에 직접 요청) const finalEndpoint = - typeof window === 'undefined' ? endpoint.replace(/^\/api/, '') : endpoint; + typeof window === 'undefined' ? endpoint.replace(/^\/api/, '/v1') : endpoint; let fullUrl = `${currentBaseUrl}${finalEndpoint}`; // 서버 환경인데 여전히 상대경로라면 강제로 도메인을 붙여줌 (방어 코드) if (typeof window === 'undefined' && !fullUrl.startsWith('http')) { - const fallback = getBackendApiBaseUrl(); - fullUrl = `${fallback}${finalEndpoint.replace(/^\/api/, '')}`; + fullUrl = `${getBackendOrigin()}${finalEndpoint}`; } const url = buildUrl(fullUrl, params); diff --git a/frontend/src/lib/api/group.ts b/frontend/src/lib/api/group.ts index 463022c54..bb7aeab3d 100644 --- a/frontend/src/lib/api/group.ts +++ b/frontend/src/lib/api/group.ts @@ -1,6 +1,6 @@ import { cache } from 'react'; import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; -import { get } from './api'; +import { get, patch } from './api'; import { DailyRecordList, GroupCoverListResponse, @@ -163,6 +163,12 @@ export const groupMyRoleOptions = (groupId: string) => staleTime: PERSONAL_STALE_TIME, }); +export const toggleGroupNotification = (groupId: string, muted: boolean) => + patch(`/api/groups/${groupId}/members/me/notification`, { muted }); + +export const markGroupAsRead = (groupId: string) => + patch(`/api/groups/${groupId}/members/me/read`, {}); + export const groupDetailOptions = (groupId: string) => queryOptions({ queryKey: ['group', groupId, 'edit'], diff --git a/frontend/src/lib/api/notification.ts b/frontend/src/lib/api/notification.ts new file mode 100644 index 000000000..42e81c008 --- /dev/null +++ b/frontend/src/lib/api/notification.ts @@ -0,0 +1,12 @@ +import { post, fetchApi } from './api'; + +export function registerFcmToken(token: string, platform: 'web' | 'android') { + return post('/api/notifications/fcm-token', { token, platform }); +} + +export function removeFcmToken(platform: 'web' | 'android') { + return fetchApi('/api/notifications/fcm-token', { + method: 'DELETE', + body: JSON.stringify({ platform }), + }); +} diff --git a/frontend/src/lib/api/records.ts b/frontend/src/lib/api/records.ts index d6dbd6d5b..759fb9b6c 100644 --- a/frontend/src/lib/api/records.ts +++ b/frontend/src/lib/api/records.ts @@ -130,7 +130,7 @@ export const mapRecordListOptions = ({ retry: false, }); -export const recordDetailOptions = (recordId: string, scope?: 'group' | 'personal') => +export const recordDetailOptions = (recordId: string) => queryOptions({ queryKey: ['record', recordId], queryFn: async () => { @@ -143,7 +143,7 @@ export const recordDetailOptions = (recordId: string, scope?: 'group' | 'persona } return response.data; }, - staleTime: scope === 'group' ? 0 : PERSONAL_STALE_TIME, + staleTime: 0, retry: false, }); diff --git a/frontend/src/lib/firebase.ts b/frontend/src/lib/firebase.ts new file mode 100644 index 000000000..e312aa19c --- /dev/null +++ b/frontend/src/lib/firebase.ts @@ -0,0 +1,20 @@ +import { initializeApp, getApps } from 'firebase/app'; +import { getMessaging, isSupported } from 'firebase/messaging'; + +const firebaseConfig = { + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, +}; + +export const firebaseApp = + getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; + +export async function getFirebaseMessaging() { + const supported = await isSupported(); + if (!supported) return null; + return getMessaging(firebaseApp); +} diff --git a/frontend/src/lib/mocks/mock.ts b/frontend/src/lib/mocks/mock.ts index ccb5ab7d7..e5cc3ead0 100644 --- a/frontend/src/lib/mocks/mock.ts +++ b/frontend/src/lib/mocks/mock.ts @@ -265,6 +265,8 @@ export const createMockGroupList = (): GroupListResponse => ({ placeName: '성수동', }, permission: 'ADMIN', + notificationMuted: false, + hasUnread: false, }, { groupId: 'group-2', @@ -286,6 +288,8 @@ export const createMockGroupList = (): GroupListResponse => ({ placeName: '협재 해수욕장', }, permission: 'ADMIN', + notificationMuted: false, + hasUnread: false, }, { groupId: 'group-3', @@ -307,6 +311,8 @@ export const createMockGroupList = (): GroupListResponse => ({ placeName: '강남역 모임장소', }, permission: 'ADMIN', + notificationMuted: false, + hasUnread: false, }, ], }); diff --git a/frontend/src/lib/types/groupResponse.ts b/frontend/src/lib/types/groupResponse.ts index e8b16cb03..c09c3f563 100644 --- a/frontend/src/lib/types/groupResponse.ts +++ b/frontend/src/lib/types/groupResponse.ts @@ -129,6 +129,7 @@ export interface GroupMembersResponse { export interface GroupMemberRoleResponse { role: GroupRoleType; + notificationMuted: boolean; } export interface GroupActivityResponse { diff --git a/frontend/src/lib/types/recordResponse.ts b/frontend/src/lib/types/recordResponse.ts index 196630fd8..27688d90c 100644 --- a/frontend/src/lib/types/recordResponse.ts +++ b/frontend/src/lib/types/recordResponse.ts @@ -70,6 +70,8 @@ export interface GroupSummary { lastActivityAt: string; latestPost: LatestPost | null; permission: 'ADMIN' | 'EDITOR' | 'VIEWER'; + notificationMuted: boolean; + hasUnread: boolean; } export interface Unread { diff --git a/mobile-app/android/app/capacitor.build.gradle b/mobile-app/android/app/capacitor.build.gradle index eb9d454b3..27c488bbb 100644 --- a/mobile-app/android/app/capacitor.build.gradle +++ b/mobile-app/android/app/capacitor.build.gradle @@ -12,9 +12,11 @@ dependencies { implementation project(':capacitor-app') implementation project(':capacitor-browser') implementation project(':capacitor-clipboard') + implementation project(':capacitor-push-notifications') implementation project(':capacitor-share') implementation project(':capacitor-splash-screen') implementation project(':capacitor-status-bar') + implementation project(':capacitor-native-settings') } diff --git a/mobile-app/android/app/google-services.json b/mobile-app/android/app/google-services.json new file mode 100644 index 000000000..ffd058a1e --- /dev/null +++ b/mobile-app/android/app/google-services.json @@ -0,0 +1,39 @@ +{ + "project_info": { + "project_number": "513351224432", + "project_id": "friendly-magnet-481309-k6", + "storage_bucket": "friendly-magnet-481309-k6.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:513351224432:android:14d3b31fb415ce5d2bb8b5", + "android_client_info": { + "package_name": "com.ittda.app" + } + }, + "oauth_client": [ + { + "client_id": "513351224432-1seqo4kpaqlrfq5sh7r0qr9f087cc3ih.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyArVb-YzJK1xFoz9CYJ9dxJAmAyBQiqP5g" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "513351224432-1seqo4kpaqlrfq5sh7r0qr9f087cc3ih.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/mobile-app/android/capacitor.settings.gradle b/mobile-app/android/capacitor.settings.gradle index c6a525713..3a9b47697 100644 --- a/mobile-app/android/capacitor.settings.gradle +++ b/mobile-app/android/capacitor.settings.gradle @@ -11,6 +11,9 @@ project(':capacitor-browser').projectDir = new File('../../node_modules/.pnpm/@c include ':capacitor-clipboard' project(':capacitor-clipboard').projectDir = new File('../../node_modules/.pnpm/@capacitor+clipboard@7.0.4_@capacitor+core@7.5.0/node_modules/@capacitor/clipboard/android') +include ':capacitor-push-notifications' +project(':capacitor-push-notifications').projectDir = new File('../../node_modules/.pnpm/@capacitor+push-notifications@8.1.1_@capacitor+core@7.5.0/node_modules/@capacitor/push-notifications/android') + include ':capacitor-share' project(':capacitor-share').projectDir = new File('../../node_modules/.pnpm/@capacitor+share@7.0.4_@capacitor+core@7.5.0/node_modules/@capacitor/share/android') @@ -19,3 +22,6 @@ project(':capacitor-splash-screen').projectDir = new File('../../node_modules/.p include ':capacitor-status-bar' project(':capacitor-status-bar').projectDir = new File('../../node_modules/.pnpm/@capacitor+status-bar@7.0.6_@capacitor+core@7.5.0/node_modules/@capacitor/status-bar/android') + +include ':capacitor-native-settings' +project(':capacitor-native-settings').projectDir = new File('../../node_modules/.pnpm/capacitor-native-settings@8.1.0_@capacitor+core@7.5.0/node_modules/capacitor-native-settings/android') diff --git a/mobile-app/package.json b/mobile-app/package.json index b45d2ebf4..02430654b 100644 --- a/mobile-app/package.json +++ b/mobile-app/package.json @@ -15,9 +15,11 @@ "@capacitor/clipboard": "^7.0.4", "@capacitor/core": "^7.0.0", "@capacitor/ios": "^7.0.0", + "@capacitor/push-notifications": "^8.1.1", "@capacitor/share": "^7.0.4", "@capacitor/splash-screen": "^7.0.5", - "@capacitor/status-bar": "^7.0.6" + "@capacitor/status-bar": "^7.0.6", + "capacitor-native-settings": "^8.1.0" }, "devDependencies": { "@capacitor/cli": "^7.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 904927127..d3f65ae92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: dotenv: specifier: ^17.2.3 version: 17.2.3 + firebase-admin: + specifier: ^14.1.0 + version: 14.1.0 logform: specifier: ^2.7.0 version: 2.7.0 @@ -243,6 +246,9 @@ importers: '@capacitor/network': specifier: ^8.0.1 version: 8.0.1(@capacitor/core@7.5.0) + '@capacitor/push-notifications': + specifier: ^8.1.1 + version: 8.1.1(@capacitor/core@7.5.0) '@capacitor/share': specifier: ^7.0.4 version: 7.0.4(@capacitor/core@7.5.0) @@ -282,6 +288,9 @@ importers: '@vis.gl/react-google-maps': specifier: ^1.7.1 version: 1.7.1(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + capacitor-native-settings: + specifier: ^8.1.0 + version: 8.1.0(@capacitor/core@7.5.0) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -294,6 +303,9 @@ importers: exifr: specifier: ^7.1.3 version: 7.1.3 + firebase: + specifier: ^12.15.0 + version: 12.15.0 framer-motion: specifier: ^12.23.26 version: 12.24.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -463,6 +475,9 @@ importers: '@capacitor/ios': specifier: ^7.0.0 version: 7.5.0(@capacitor/core@7.5.0) + '@capacitor/push-notifications': + specifier: ^8.1.1 + version: 8.1.1(@capacitor/core@7.5.0) '@capacitor/share': specifier: ^7.0.4 version: 7.0.4(@capacitor/core@7.5.0) @@ -472,6 +487,9 @@ importers: '@capacitor/status-bar': specifier: ^7.0.6 version: 7.0.6(@capacitor/core@7.5.0) + capacitor-native-settings: + specifier: ^8.1.0 + version: 8.1.0(@capacitor/core@7.5.0) devDependencies: '@capacitor/cli': specifier: ^7.0.0 @@ -956,6 +974,11 @@ packages: peerDependencies: '@capacitor/core': '>=8.0.0' + '@capacitor/push-notifications@8.1.1': + resolution: {integrity: sha512-WqzjPKIbYbARMN+GC0XMAJcxJpUUzqgzS/Ny8RODLrro38pQhm3GXYwX2Mwd+LZlLY39rGImkCkrKyQSNfuikA==} + peerDependencies: + '@capacitor/core': '>=8.0.0' + '@capacitor/share@7.0.4': resolution: {integrity: sha512-wNXxmXUcChrtbZ5jQv8scFIbIGKo3rk6B7qpWySxfUhYJP5NANgAqQ9Z69m0k/zYsucdh66SBU7XW1ykd9aUug==} peerDependencies: @@ -1210,6 +1233,219 @@ packages: resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + + '@firebase/ai@2.13.1': + resolution: {integrity: sha512-RhT/VViTPBSplhQSuEp62HhLvfsV+LowMh8ZUo5MMRDzG7oFtSget4Kmg5oHP50hDVyWQuQj6to9iPFEZk08Tw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-types': 0.x + + '@firebase/analytics-compat@0.2.28': + resolution: {integrity: sha512-lIAlqUUbBu93FJMlQfslryQtBwwzdzvp23ePC6FNgymXk6Ook5v4Uvc0vdutvoIeqmyA3LfP0ZeRFK8+11kOOQ==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/analytics-types@0.8.4': + resolution: {integrity: sha512-zQ+XTgkwH6CY/eUSHJRP7e4LxM30RCxlCmob5sy2axs25GE3Ny0XdgpDscMTHHQIGqWkxPXad4w2Mw9sCgT8zQ==} + + '@firebase/analytics@0.10.22': + resolution: {integrity: sha512-8BSaq/QRGU1+xyi8L2PTLTJU7MH9aMA72RQdIxrbhWFauOZY9OXo8f2YDN/972xA8d588tlnNVEQ2Mo69pT9Ow==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-check-compat@0.4.5': + resolution: {integrity: sha512-JI17mVcZs34zO6ZeSCrw4U2iohqy+n6GIzkbmsA+TbVjmvFLkUKt3bs5M+qRBteQm/0IWzqSHYFzEQLzDTQebg==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/app-check-interop-types@0.3.4': + resolution: {integrity: sha512-zz3i6e13B8BfWiLy8MABtTh8aGIACgKbf9UVnyHcWs+yQzJXgQcl8A46b0zfaiJHdQ+niF0ouAfcpuf+3LMPQg==} + + '@firebase/app-check-types@0.5.4': + resolution: {integrity: sha512-xV7JsIyzVr15aA7f3Pi0rB9gdBuVubs89FGA8VkRYA4g0l78poADgdfrScgf7NndSg9mm7cR7PJyY0+t22KaGw==} + + '@firebase/app-check@0.12.0': + resolution: {integrity: sha512-wMeT6HLWRAuW7Cp/5UjWBGKgjPNxWNOoNf4PRIv0weljoGMZVeqbUY7wNBWTI2/31cX1NlXx8gQruDLsUShB3Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-compat@0.5.14': + resolution: {integrity: sha512-rgFmiofYsdS9ZG/Bht3OBxJtPD3zWE1cffShWubEm+4+qZeyzCbmtb1q6jOEjN9fB7uufe4rQmWOPXouR3758Q==} + engines: {node: '>=20.0.0'} + + '@firebase/app-types@0.9.5': + resolution: {integrity: sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==} + + '@firebase/app@0.15.0': + resolution: {integrity: sha512-soIskolmGgbpi0K/MfrjtdpO1220qRCbXA4Z8Qx3lM+fVwA3q40m+OM+7zBHd2nuQCrLXb33L6Oc1aBH3Y26AQ==} + engines: {node: '>=20.0.0'} + + '@firebase/auth-compat@0.6.8': + resolution: {integrity: sha512-llcBREUC4iSNKZ6rvwud7Oz9Q7aAWU6KuQLa6pdu7Q+QAQsy4JLw6yFgxwtmzabsgznHmmcsX2UjHLLzqUxi3Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/auth-interop-types@0.2.5': + resolution: {integrity: sha512-1Li/YuBDBAXcKv7BzY4U28gontUmAaw53sYiqbaVOMCFb2lFKK/c3CGMUWqtwe7+TXrl3poWnTCL5umYBg85Eg==} + + '@firebase/auth-types@0.13.1': + resolution: {integrity: sha512-0c1Mnid0uMDfGJHeUS4zfvBa4/CedJXotGy/n/NZJnBjwiJawt0ZYU+wH2VAVLiRCEfG2ncCkAX3yd1/2nrB7g==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/auth@1.13.3': + resolution: {integrity: sha512-bqiq4uubDN2YyQkdvSWPQeJyXAv2O76ImF41En9b6UhV5JuBVYDoHYrrrE3NzIuGkpFMKagfhMRP4Vz6t+yQSQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@react-native-async-storage/async-storage': ^2.2.0 || ^3.0.0 + peerDependenciesMeta: + '@react-native-async-storage/async-storage': + optional: true + + '@firebase/component@0.7.3': + resolution: {integrity: sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==} + engines: {node: '>=20.0.0'} + + '@firebase/data-connect@0.7.1': + resolution: {integrity: sha512-2LbUU8mmSA63HknxQMmWHjpzuNLBKflvVwQc2tpoVKg0biWleNEJX031ELks0vzFs+dDjOUkCJR72RP6mQHFOg==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/database-compat@2.1.4': + resolution: {integrity: sha512-3pK35F1MAgmqFJQlf2nhQl44vtAXQO1uaCaQOEUI9kCRtLFqi7N+QRKR7lFZPg+xIZIyubgxQaxY69YgfZRZWg==} + engines: {node: '>=20.0.0'} + + '@firebase/database-types@1.0.20': + resolution: {integrity: sha512-kegbOk/w8iU64pr0q6k2ItyNGjnQBMHFhwS7ohdWI4W+pc0/zhhdGXTdFj6X1oxItRjPoYOsSQmERgBkn/ihxw==} + + '@firebase/database@1.1.3': + resolution: {integrity: sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==} + engines: {node: '>=20.0.0'} + + '@firebase/firestore-compat@0.4.11': + resolution: {integrity: sha512-W7o1WdwWq5aABK5Up2ncSvTQs/QGLR/fy7cVpFBNqhsXtxoMtflHf2xBIG6+aoptcuGAobddq4g2Sq27wqHaYw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/firestore-types@3.0.4': + resolution: {integrity: sha512-jGn+JSS4X9zZsrfu7Yw66v5YRdOLD1oyQh4USR0xWl4CUqV/DA6bNIXRPpxH/cUl3iVTNiP6MN7g+EL42A4qfA==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/firestore@4.16.0': + resolution: {integrity: sha512-qdHMHMvMr0nRMuZyWNR/ArWa0YlPE3C4eAbmxTASJMYXAesKPL0Y54p70moggrNPzaK7MSIIq5RDJJyntQyIYA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/functions-compat@0.4.5': + resolution: {integrity: sha512-10qlUXGY25G5/1g9UihqksPp2po+ZqSE7LEizsrdUP7vrTmkysXxGSZCDyojSEp6mQe/ecRDdDDI+z4XRdb4wQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/functions-types@0.6.4': + resolution: {integrity: sha512-zV6kgqtduR4rUAdC/ilS7kmb93XD7bEZoJDlVBZqlOw2uGGGCNBQBuleww2rr0Ulr3L9o2TDjumEt68/l1f9DQ==} + + '@firebase/functions@0.13.5': + resolution: {integrity: sha512-bWCx713f4kE/uFV7gdFOLBS7lDoiZj48MRkbAqe35gkXcCeWF4QjRNO07Jhmve7EJIoQOBczL29y2r8VRuN1kw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/installations-compat@0.2.22': + resolution: {integrity: sha512-C/zpAuTP5S9OgKSPvXRupw3hoY/JZSlA1wFjD/Sb7LIQE0FNbcMdO8Y4KXVEkjVzma/DDDDIAzxEXqKMAzc88w==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/installations-types@0.5.4': + resolution: {integrity: sha512-U2eFapdHwjb43Vx9o+Pmj4dFfvcHEK1IirEFLqMtWrTHvmdrS3gBpBD1kmJk/9HjsOtoHZxJ2Paoe79e+L1ZPg==} + peerDependencies: + '@firebase/app-types': 0.x + + '@firebase/installations@0.6.22': + resolution: {integrity: sha512-ef6nn3GGQTdReCfotRMG77PJZu8CqEbiK5pEoBnM0gTu/Z9v0i/az2p3HABsa/1beQmmyh1OsOjf7P5+pgwdZw==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/logger@0.5.1': + resolution: {integrity: sha512-vZKLsqE1ABOy8OjQiE7cUTFn4gvaqlk88yp8N94Pk/sDpq61YqZGqmVFZTvOyflTwuYFcWirBdYGoJgbDaXKYQ==} + engines: {node: '>=20.0.0'} + + '@firebase/messaging-compat@0.2.27': + resolution: {integrity: sha512-JNOiu1PPgdHzEPEtoFiNxQuu0x9bm4bfETSQCpGfcTlgWkhlSK7uh7nlsjC10TQLUNgYetLmuutaYTh8aeYLVA==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/messaging-interop-types@0.2.5': + resolution: {integrity: sha512-tUEKnaAP2Y/MNIqgnriPpV6e5l13Vs/+p2yrd6NGlncPJT9O3a8muYZtdnWe+IJ4fgKLHJVC79n/asxk/N5Msw==} + + '@firebase/messaging@0.13.0': + resolution: {integrity: sha512-GZoo0uGRvEbszo83xcgbjJp4FpkmBEr4l8Z4hi8gl+P1Spn/MTK3HapanMzSX4yUHuTEiF5hasWRxOaz+o5sxQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/performance-compat@0.2.25': + resolution: {integrity: sha512-q6NjTXpIPoFuUmCmMN/maCdTgzT6aExs9xZo+PxfVLj6uLVGvpyAD6XWjmcrb7jChsFBYbq7E5dyNDF7Zhy9kA==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/performance-types@0.2.4': + resolution: {integrity: sha512-kJSEk7b0uhpcPRyL4SQ/GPujLqk52XNKcXlnsKDbWGAb9vugcLvOU3u6zfEdwd+d8hWJb5S5ZizV1JFFI0nkKg==} + + '@firebase/performance@0.7.12': + resolution: {integrity: sha512-fe7nV8teUU3OBHlMUZ9Lw4gLhCW2k4m5Uc3pfWGV+fl8uwJQBGp9Q3lqsJ+HSrFu3Q2pJyLAgrClPGSKyDeYgQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/remote-config-compat@0.2.26': + resolution: {integrity: sha512-uC57Tc7GYYOCnMgLkGIVf999XlaYaPDONoa54c93YTKDctlvCZI89z0zQ2RbhGR8Zf+QuCbQHs/99vqoE84a7g==} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/remote-config-types@0.5.1': + resolution: {integrity: sha512-cX/1LT6KQwkXzck2eSzeKnuvXZCyr8qaPpDcikoJs7jmI+oBOXixpDLeDtWj1U6GNMkIoXrEDNoyT2Ypcyp5/A==} + + '@firebase/remote-config@0.8.5': + resolution: {integrity: sha512-zb+7CDGFP2wYVF1LXQoYIFdoESIQM3p0+uiW1welw8+zvDxAL50K75PKTXXtunJADUrksTVpV7mD0pn54vzJRA==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/storage-compat@0.4.3': + resolution: {integrity: sha512-gruVqjtUGX8tEoeNbaWXZm0Zfcfcb7fvmDmBxV8yPAbWvExRnZYLO2+qw9idxNE7BvPXt5csyjSYHy//dAizxw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app-compat': 0.x + + '@firebase/storage-types@0.8.4': + resolution: {integrity: sha512-BT7cwxJOx8SWwlQfrlC+bD/Sk3Cw+1odCi8UZNFNWTVZoPsBnA5W+mqtZzVnvsdJpXCFGSGQ7R7vOR6dtM/BRA==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/storage@0.14.3': + resolution: {integrity: sha512-YX4/YL6P6/fufSSeGnVhjWddcIXbFq2cWIhMKFTZo1E/Rtcl2mJj/BYUQTwJfcE1Tl8un1FOya4L05jcSLN/Eg==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/util@1.15.1': + resolution: {integrity: sha512-LUdM4Wg7YM9Pq/49nGYySJA0CSQEKnGffFzWV8+6gXN7mGxn+FL1IqvFbuZUtAQcfZgHYDwCE1wwlK7rB7gl2g==} + engines: {node: '>=20.0.0'} + + '@firebase/webchannel-wrapper@1.0.6': + resolution: {integrity: sha512-Vr/Mqu79dMwGRAyGbJ4uN4+BtXB3/mRTdzetD1daWNeG8QaWuzhhbG77GltO5c0yYmYls8i250iX73624GJd7Q==} + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -1240,9 +1476,47 @@ packages: '@formatjs/intl-localematcher@0.6.2': resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + '@google-cloud/firestore@8.6.0': + resolution: {integrity: sha512-TdvZHfwQj5B5CSDEgDqyrhdVqtOSupmBXDQPasMAJiC64tjsGvyMooNiC43fdk1TsUHeklyoZ6/vQ1TjWKVMbg==} + engines: {node: '>=18'} + + '@google-cloud/paginator@5.0.2': + resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} + engines: {node: '>=14.0.0'} + + '@google-cloud/projectify@4.0.0': + resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} + engines: {node: '>=14.0.0'} + + '@google-cloud/promisify@4.0.0': + resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} + engines: {node: '>=14'} + + '@google-cloud/storage@7.21.0': + resolution: {integrity: sha512-l+IFTkd+6Y5LoAuXyYCKNAKtw/Ci+rAMqgdTB1jv4iZiLhw0rtq+0qjIRbBizXkNzEFmXiXUW0H7sZQQvk1ffA==} + engines: {node: '>=14'} + '@googlemaps/markerclusterer@2.6.2': resolution: {integrity: sha512-U6uVhq8iWhiIckA89sgRu8OK35mjd6/3CuoZKWakKEf0QmRRWpatlsPb3kqXkoWSmbcZkopRiI4dnW6DQSd7bQ==} + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + + '@grpc/grpc-js@1.9.16': + resolution: {integrity: sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==} + engines: {node: ^8.13.0 || >=10.10.0} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1708,6 +1982,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@lhci/cli@0.15.1': resolution: {integrity: sha512-yhC0oXnXqGHYy1xl4D8YqaydMZ/khFAnXGY/o2m/J3PqPa/D0nj3V6TLoH02oVMFeEF2AQim7UbmdXMiXx2tOw==} hasBin: true @@ -2063,6 +2340,9 @@ packages: resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2312,6 +2592,33 @@ packages: peerDependencies: '@opentelemetry/api': ^1.8 + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@puppeteer/browsers@2.13.0': resolution: {integrity: sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==} engines: {node: '>=18'} @@ -3439,6 +3746,10 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tootallnate/once@2.0.1': + resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} + engines: {node: '>= 10'} + '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} @@ -3475,6 +3786,9 @@ packages: '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/caseless@0.12.5': + resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -3653,6 +3967,9 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} + '@types/request@2.48.13': + resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} + '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} @@ -3683,6 +4000,9 @@ packages: '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} @@ -3762,6 +4082,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -4024,6 +4345,7 @@ packages: '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -4031,6 +4353,10 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -4160,6 +4486,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + app-root-path@3.1.0: resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} engines: {node: '>= 6.0.0'} @@ -4228,6 +4557,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + arrify@2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -4257,6 +4590,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -4395,11 +4731,15 @@ packages: basic-ftp@5.2.0: resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.1, please upgrade big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + bin-version-check@5.1.0: resolution: {integrity: sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g==} engines: {node: '>=12'} @@ -4521,6 +4861,11 @@ packages: caniuse-lite@1.0.30001766: resolution: {integrity: sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==} + capacitor-native-settings@8.1.0: + resolution: {integrity: sha512-bcIsKO0EBChdiG2l7RAt70cKxz0Vygfx7quN6tSoNlwziCct8f5EgT8xmwNit+7F712ojl54xNaEddu87EpkjQ==} + peerDependencies: + '@capacitor/core': '>=8.0.2' + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -4903,6 +5248,10 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} @@ -5113,6 +5462,9 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + duplexify@4.1.3: + resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -5418,6 +5770,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -5463,6 +5819,9 @@ packages: resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==} engines: {node: '>=4'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + external-editor@3.1.0: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} @@ -5472,6 +5831,10 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + farmhash-modern@1.1.0: + resolution: {integrity: sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==} + engines: {node: '>=18.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -5501,13 +5864,24 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-builder@1.2.1: + resolution: {integrity: sha512-tPb5TTWfgfVx5BNSi2xV0eLr89POeXXn0dXIsCJ9m1narrWxeIyx6je9d7Rce/3NyXLbvuQmLkxq+RuxMWejvw==} + fast-xml-parser@5.2.5: resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} hasBin: true + fast-xml-parser@5.9.3: + resolution: {integrity: sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} @@ -5526,6 +5900,10 @@ packages: fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -5584,6 +5962,13 @@ packages: resolution: {integrity: sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==} engines: {node: '>=12'} + firebase-admin@14.1.0: + resolution: {integrity: sha512-GdHh6vHWm9LVRt+3hINWczaA7fPwnN/l4xZdqzn+wNPYErrLI1x6u1mvAJM/5IGgYI9EqQgLt1EyBG8ok/hWCg==} + engines: {node: '>=22'} + + firebase@12.15.0: + resolution: {integrity: sha512-p0YTLcRSTiBXMx9sGr4ZNSfLjc/RVBEw4C/TXjVMtw65+6E1Pbm47UY3F4/AqRoDobEcNX3gsbPGy7jPjxbgSQ==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -5613,10 +5998,18 @@ packages: resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} engines: {node: '>= 14.17'} + form-data@2.5.6: + resolution: {integrity: sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==} + engines: {node: '>= 0.12'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + formidable@3.5.4: resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} engines: {node: '>=14.0.0'} @@ -5685,9 +6078,36 @@ packages: resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} engines: {node: '>= 0.4'} + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gaxios@7.1.3: + resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==} + engines: {node: '>=18'} + + gaxios@7.1.5: + resolution: {integrity: sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==} + engines: {node: '>=18'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + gcp-metadata@8.1.3: + resolution: {integrity: sha512-ziTrzUhhpL9Zk5k0HHzgP/KIpWDJT0VMBC/ynt/QIBvTW+UUcSivQRl6VlwTf/EilDxtSWklHoRsKy1c4k+59w==} + engines: {node: '>=18'} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -5792,6 +6212,30 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + google-auth-library@10.5.0: + resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==} + engines: {node: '>=18'} + + google-auth-library@10.9.0: + resolution: {integrity: sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==} + engines: {node: '>=18'} + + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-gax@5.0.7: + resolution: {integrity: sha512-EhiqaWWJ+9h7sCcKJTsoo6tMcjokVHhWsbSuWCnZJT4vIBP3y4mAoFLnt9SzgkVZeq24ZsFaArr06nnYYku2yA==} + engines: {node: '>=18'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -5807,6 +6251,14 @@ packages: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + + gtoken@8.0.0: + resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==} + engines: {node: '>=18'} + gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} @@ -5847,6 +6299,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} @@ -5856,6 +6312,9 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + html-entities@2.6.0: + resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -5870,6 +6329,13 @@ packages: resolution: {integrity: sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==} engines: {node: '>=6.0.0'} + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -5907,6 +6373,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -6157,6 +6626,9 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-unsafe@1.0.1: + resolution: {integrity: sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -6419,6 +6891,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -6463,6 +6938,10 @@ packages: jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + jwks-rsa@4.1.0: + resolution: {integrity: sha512-sbkByqyATKYJP5F4RXj03N5TUNC0QLTjCAZvwTzC4BwJZ8e0/cWxN8YROnyUth2g1/ONWi4eSFHeu6oYalrc3Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >= 23.0.0} + jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} @@ -6595,6 +7074,9 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + limiter@1.1.5: + resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -6629,6 +7111,12 @@ packages: lodash-es@4.17.23: resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -6671,6 +7159,9 @@ packages: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lookup-closest-locale@6.2.0: resolution: {integrity: sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==} @@ -6699,6 +7190,9 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} + lru-memoizer@3.0.0: + resolution: {integrity: sha512-m83w/cYXLdUIboKSPxzPAGfYnk+vqeDYXuoSrQRw1q+yVEd8IXhvMufN8Q5TIPe7e2jyX4SRNrDJI2Skw1yznQ==} + lucide-react@0.561.0: resolution: {integrity: sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==} peerDependencies: @@ -6813,6 +7307,11 @@ packages: engines: {node: '>=4.0.0'} hasBin: true + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-fn@1.2.0: resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} engines: {node: '>=4'} @@ -7018,6 +7517,11 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} @@ -7030,6 +7534,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -7237,6 +7745,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.6.1: + resolution: {integrity: sha512-h7bxdzhHk8Knyc4Tj+jMaa7fEEoUJy7p1qtbVgkYg1Uhpe5Np5VuGXCRZnkZvU+Q42M1vStt0ifa3ueykRJPmQ==} + engines: {node: '>=14.0.0'} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -7454,6 +7966,14 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proto3-json-serializer@3.0.4: + resolution: {integrity: sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==} + engines: {node: '>=18'} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -7505,6 +8025,9 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + re2js@0.4.3: + resolution: {integrity: sha512-EuNmh7jurhHEE8Ge/lBo9JuMLb3qf866Xjjfyovw3wPc7+hlqDkZq4LwhrCQMEI+ARWfrKrHozEndzlpNT0WDg==} + react-day-picker@9.12.0: resolution: {integrity: sha512-t8OvG/Zrciso5CQJu5b1A7yzEmebvST+S3pOVQJWxwjjVngyG/CA2htN/D15dLI4uTEuLLkbZyS4YYt480FAtA==} engines: {node: '>=18'} @@ -7697,6 +8220,18 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry-request@7.0.2: + resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} + engines: {node: '>=14'} + + retry-request@8.0.3: + resolution: {integrity: sha512-qqoc4kkGgP9cmQDWELlOpAmfgJOg0Yi7MT82ZjiPWu451ayju4itwomjM4/dBEliify8C1b3tSaeCOldugtwPQ==} + engines: {node: '>=18'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + rettime@0.7.0: resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} @@ -7717,6 +8252,10 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rimraf@5.0.10: + resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} + hasBin: true + rimraf@6.1.3: resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} engines: {node: 20 || >=22} @@ -8044,6 +8583,12 @@ packages: prettier: optional: true + stream-events@1.0.5: + resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + + stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -8154,10 +8699,16 @@ packages: strnum@2.1.2: resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} + strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} + stubs@3.0.0: + resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -8242,6 +8793,14 @@ packages: resolution: {integrity: sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==} engines: {node: '>=18'} + teeny-request@10.1.3: + resolution: {integrity: sha512-5yDliI1uWkYPo7W+Zvrxg6YmoWuj5iC5EydewqrRTvc68nyMTZhlPPlLg6cptUGfbQAb+N9XDPDPzF6N081lug==} + engines: {node: '>=18'} + + teeny-request@9.0.0: + resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} + engines: {node: '>=14'} + teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} @@ -8282,8 +8841,8 @@ packages: third-party-web@0.26.7: resolution: {integrity: sha512-buUzX4sXC4efFX6xg2bw6/eZsCUh8qQwSavC4D9HpONMFlRbcHhD8Je5qwYdCpViR6q0qla2wPP+t91a2vgolg==} - third-party-web@0.29.0: - resolution: {integrity: sha512-nBDSJw5B7Sl1YfsATG2XkW5qgUPODbJhXw++BKygi9w6O/NKS98/uY/nR/DxDq2axEjL6halHW1v+jhm/j1DBQ==} + third-party-web@0.29.2: + resolution: {integrity: sha512-fegtha91tq2DHphyoiBXVHjVi2YG9zFaRnboT9C28tO1en9Y3wJsfspuy40F+u5wl3hHVbw7cnd1b67kEGHb8g==} through2@4.0.2: resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} @@ -8437,6 +8996,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -8697,10 +9257,12 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -8826,6 +9388,13 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webdriver-bidi-protocol@0.4.1: resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==} @@ -8861,6 +9430,14 @@ packages: webpack-cli: optional: true + websocket-driver@0.7.5: + resolution: {integrity: sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==} + engines: {node: '>=0.8.0'} + + websocket-extensions@0.1.4: + resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} + engines: {node: '>=0.8.0'} + whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} @@ -8991,6 +9568,10 @@ packages: resolution: {integrity: sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==} engines: {node: '>=8'} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -9981,6 +10562,10 @@ snapshots: dependencies: '@capacitor/core': 7.5.0 + '@capacitor/push-notifications@8.1.1(@capacitor/core@7.5.0)': + dependencies: + '@capacitor/core': 7.5.0 + '@capacitor/share@7.0.4(@capacitor/core@7.5.0)': dependencies: '@capacitor/core': 7.5.0 @@ -10168,26 +10753,349 @@ snapshots: '@faker-js/faker@9.9.0': {} - '@floating-ui/core@1.7.3': - dependencies: - '@floating-ui/utils': 0.2.10 + '@fastify/busboy@3.2.0': {} - '@floating-ui/dom@1.7.4': + '@firebase/ai@2.13.1(@firebase/app-types@0.9.5)(@firebase/app@0.15.0)': dependencies: - '@floating-ui/core': 1.7.3 - '@floating-ui/utils': 0.2.10 + '@firebase/app': 0.15.0 + '@firebase/app-check-interop-types': 0.3.4 + '@firebase/app-types': 0.9.5 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 - '@floating-ui/react-dom@2.1.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + '@firebase/analytics-compat@0.2.28(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': dependencies: - '@floating-ui/dom': 1.7.4 - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) + '@firebase/analytics': 0.10.22(@firebase/app@0.15.0) + '@firebase/analytics-types': 0.8.4 + '@firebase/app-compat': 0.5.14 + '@firebase/component': 0.7.3 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' - '@floating-ui/utils@0.2.10': {} + '@firebase/analytics-types@0.8.4': {} - '@formatjs/ecma402-abstract@2.3.6': + '@firebase/analytics@0.10.22(@firebase/app@0.15.0)': dependencies: - '@formatjs/fast-memoize': 2.2.7 + '@firebase/app': 0.15.0 + '@firebase/component': 0.7.3 + '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/app-check-compat@0.4.5(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + dependencies: + '@firebase/app-check': 0.12.0(@firebase/app@0.15.0) + '@firebase/app-check-types': 0.5.4 + '@firebase/app-compat': 0.5.14 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/app-check-interop-types@0.3.4': {} + + '@firebase/app-check-types@0.5.4': {} + + '@firebase/app-check@0.12.0(@firebase/app@0.15.0)': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/app-compat@0.5.14': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/app-types@0.9.5': + dependencies: + '@firebase/logger': 0.5.1 + + '@firebase/app@0.15.0': + dependencies: + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/auth-compat@0.6.8(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0)': + dependencies: + '@firebase/app-compat': 0.5.14 + '@firebase/auth': 1.13.3(@firebase/app@0.15.0) + '@firebase/auth-types': 0.13.1(@firebase/app-types@0.9.5)(@firebase/util@1.15.1) + '@firebase/component': 0.7.3 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + - '@react-native-async-storage/async-storage' + + '@firebase/auth-interop-types@0.2.5': {} + + '@firebase/auth-types@0.13.1(@firebase/app-types@0.9.5)(@firebase/util@1.15.1)': + dependencies: + '@firebase/app-types': 0.9.5 + '@firebase/util': 1.15.1 + + '@firebase/auth@1.13.3(@firebase/app@0.15.0)': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/component@0.7.3': + dependencies: + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/data-connect@0.7.1(@firebase/app@0.15.0)': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/auth-interop-types': 0.2.5 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/database-compat@2.1.4': + dependencies: + '@firebase/component': 0.7.3 + '@firebase/database': 1.1.3 + '@firebase/database-types': 1.0.20 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/database-types@1.0.20': + dependencies: + '@firebase/app-types': 0.9.5 + '@firebase/util': 1.15.1 + + '@firebase/database@1.1.3': + dependencies: + '@firebase/app-check-interop-types': 0.3.4 + '@firebase/auth-interop-types': 0.2.5 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + faye-websocket: 0.11.4 + tslib: 2.8.1 + + '@firebase/firestore-compat@0.4.11(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0)': + dependencies: + '@firebase/app-compat': 0.5.14 + '@firebase/component': 0.7.3 + '@firebase/firestore': 4.16.0(@firebase/app@0.15.0) + '@firebase/firestore-types': 3.0.4(@firebase/app-types@0.9.5)(@firebase/util@1.15.1) + '@firebase/util': 1.15.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/firestore-types@3.0.4(@firebase/app-types@0.9.5)(@firebase/util@1.15.1)': + dependencies: + '@firebase/app-types': 0.9.5 + '@firebase/util': 1.15.1 + + '@firebase/firestore@4.16.0(@firebase/app@0.15.0)': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + '@firebase/webchannel-wrapper': 1.0.6 + '@grpc/grpc-js': 1.9.16 + '@grpc/proto-loader': 0.7.15 + re2js: 0.4.3 + tslib: 2.8.1 + + '@firebase/functions-compat@0.4.5(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + dependencies: + '@firebase/app-compat': 0.5.14 + '@firebase/component': 0.7.3 + '@firebase/functions': 0.13.5(@firebase/app@0.15.0) + '@firebase/functions-types': 0.6.4 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/functions-types@0.6.4': {} + + '@firebase/functions@0.13.5(@firebase/app@0.15.0)': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/app-check-interop-types': 0.3.4 + '@firebase/auth-interop-types': 0.2.5 + '@firebase/component': 0.7.3 + '@firebase/messaging-interop-types': 0.2.5 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/installations-compat@0.2.22(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0)': + dependencies: + '@firebase/app-compat': 0.5.14 + '@firebase/component': 0.7.3 + '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/installations-types': 0.5.4(@firebase/app-types@0.9.5) + '@firebase/util': 1.15.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/installations-types@0.5.4(@firebase/app-types@0.9.5)': + dependencies: + '@firebase/app-types': 0.9.5 + + '@firebase/installations@0.6.22(@firebase/app@0.15.0)': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/component': 0.7.3 + '@firebase/util': 1.15.1 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/logger@0.5.1': + dependencies: + tslib: 2.8.1 + + '@firebase/messaging-compat@0.2.27(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + dependencies: + '@firebase/app-compat': 0.5.14 + '@firebase/component': 0.7.3 + '@firebase/messaging': 0.13.0(@firebase/app@0.15.0) + '@firebase/util': 1.15.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/messaging-interop-types@0.2.5': {} + + '@firebase/messaging@0.13.0(@firebase/app@0.15.0)': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/component': 0.7.3 + '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/messaging-interop-types': 0.2.5 + '@firebase/util': 1.15.1 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/performance-compat@0.2.25(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + dependencies: + '@firebase/app-compat': 0.5.14 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/performance': 0.7.12(@firebase/app@0.15.0) + '@firebase/performance-types': 0.2.4 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/performance-types@0.2.4': {} + + '@firebase/performance@0.7.12(@firebase/app@0.15.0)': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/component': 0.7.3 + '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + web-vitals: 4.2.4 + + '@firebase/remote-config-compat@0.2.26(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0)': + dependencies: + '@firebase/app-compat': 0.5.14 + '@firebase/component': 0.7.3 + '@firebase/logger': 0.5.1 + '@firebase/remote-config': 0.8.5(@firebase/app@0.15.0) + '@firebase/remote-config-types': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + + '@firebase/remote-config-types@0.5.1': {} + + '@firebase/remote-config@0.8.5(@firebase/app@0.15.0)': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/component': 0.7.3 + '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/logger': 0.5.1 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/storage-compat@0.4.3(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0)': + dependencies: + '@firebase/app-compat': 0.5.14 + '@firebase/component': 0.7.3 + '@firebase/storage': 0.14.3(@firebase/app@0.15.0) + '@firebase/storage-types': 0.8.4(@firebase/app-types@0.9.5)(@firebase/util@1.15.1) + '@firebase/util': 1.15.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app' + - '@firebase/app-types' + + '@firebase/storage-types@0.8.4(@firebase/app-types@0.9.5)(@firebase/util@1.15.1)': + dependencies: + '@firebase/app-types': 0.9.5 + '@firebase/util': 1.15.1 + + '@firebase/storage@0.14.3(@firebase/app@0.15.0)': + dependencies: + '@firebase/app': 0.15.0 + '@firebase/component': 0.7.3 + '@firebase/util': 1.15.1 + tslib: 2.8.1 + + '@firebase/util@1.15.1': + dependencies: + tslib: 2.8.1 + + '@firebase/webchannel-wrapper@1.0.6': {} + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + + '@floating-ui/utils@0.2.10': {} + + '@formatjs/ecma402-abstract@2.3.6': + dependencies: + '@formatjs/fast-memoize': 2.2.7 '@formatjs/intl-localematcher': 0.6.2 decimal.js: 10.6.0 tslib: 2.8.1 @@ -10211,12 +11119,82 @@ snapshots: dependencies: tslib: 2.8.1 + '@google-cloud/firestore@8.6.0': + dependencies: + '@opentelemetry/api': 1.9.0 + fast-deep-equal: 3.1.3 + functional-red-black-tree: 1.0.1 + google-gax: 5.0.7 + protobufjs: 7.6.5 + transitivePeerDependencies: + - supports-color + optional: true + + '@google-cloud/paginator@5.0.2': + dependencies: + arrify: 2.0.1 + extend: 3.0.2 + optional: true + + '@google-cloud/projectify@4.0.0': + optional: true + + '@google-cloud/promisify@4.0.0': + optional: true + + '@google-cloud/storage@7.21.0': + dependencies: + '@google-cloud/paginator': 5.0.2 + '@google-cloud/projectify': 4.0.0 + '@google-cloud/promisify': 4.0.0 + abort-controller: 3.0.0 + async-retry: 1.3.3 + duplexify: 4.1.3 + fast-xml-parser: 5.9.3 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + html-entities: 2.6.0 + mime: 3.0.0 + p-limit: 3.1.0 + retry-request: 7.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + '@googlemaps/markerclusterer@2.6.2': dependencies: '@types/supercluster': 7.1.3 fast-equals: 5.4.0 supercluster: 8.0.1 + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + optional: true + + '@grpc/grpc-js@1.9.16': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@types/node': 25.5.0 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.5 + yargs: 17.7.2 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.5 + yargs: 17.7.2 + optional: true + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -10641,7 +11619,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.7 + '@types/node': 25.5.0 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -10659,7 +11637,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.19.7 + '@types/node': 25.5.0 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -10835,6 +11813,9 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-sdsl/ordered-map@4.4.2': + optional: true + '@lhci/cli@0.15.1': dependencies: '@lhci/utils': 0.15.1 @@ -11194,6 +12175,9 @@ snapshots: '@noble/hashes@1.8.0': {} + '@nodable/entities@2.2.0': + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -11480,7 +12464,7 @@ snapshots: '@paulirish/trace_engine@0.0.53': dependencies: legacy-javascript: 0.0.1 - third-party-web: 0.29.0 + third-party-web: 0.29.2 '@pkgjs/parseargs@0.11.0': optional: true @@ -11496,6 +12480,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + '@puppeteer/browsers@2.13.0': dependencies: debug: 4.4.3 @@ -12769,6 +13773,9 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@tootallnate/once@2.0.1': + optional: true + '@tootallnate/quickjs-emscripten@0.23.0': {} '@tsconfig/node10@1.0.12': {} @@ -12812,6 +13819,9 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.19.7 + '@types/caseless@0.12.5': + optional: true + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -12894,7 +13904,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.19.7 + '@types/node': 25.5.0 '@types/http-cache-semantics@4.0.4': {} @@ -12938,7 +13948,7 @@ snapshots: '@types/mysql@2.15.27': dependencies: - '@types/node': 22.19.7 + '@types/node': 25.5.0 '@types/node@20.19.27': dependencies: @@ -12997,7 +14007,7 @@ snapshots: '@types/pg@8.15.6': dependencies: - '@types/node': 22.19.7 + '@types/node': 25.5.0 pg-protocol: 1.10.3 pg-types: 2.2.0 @@ -13013,6 +14023,14 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/request@2.48.13': + dependencies: + '@types/caseless': 0.12.5 + '@types/node': 25.5.0 + '@types/tough-cookie': 4.0.5 + form-data: 2.5.6 + optional: true + '@types/resolve@1.20.6': {} '@types/send@1.2.1': @@ -13048,7 +14066,10 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 22.19.7 + '@types/node': 25.5.0 + + '@types/tough-cookie@4.0.5': + optional: true '@types/triple-beam@1.3.5': {} @@ -13064,7 +14085,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.19.7 + '@types/node': 25.5.0 optional: true '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': @@ -13524,6 +14545,11 @@ snapshots: '@xtuc/long@4.2.2': {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + optional: true + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -13630,6 +14656,9 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + anynum@1.0.1: + optional: true + app-root-path@3.1.0: {} append-field@1.0.0: {} @@ -13725,6 +14754,9 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + arrify@2.0.1: + optional: true + asap@2.0.6: {} assertion-error@2.0.1: {} @@ -13749,6 +14781,11 @@ snapshots: async-function@1.0.0: {} + async-retry@1.3.3: + dependencies: + retry: 0.13.1 + optional: true + async@3.2.6: {} asynckit@0.4.0: {} @@ -13911,6 +14948,8 @@ snapshots: big-integer@1.6.52: {} + bignumber.js@9.3.1: {} + bin-version-check@5.1.0: dependencies: bin-version: 6.0.0 @@ -14065,6 +15104,10 @@ snapshots: caniuse-lite@1.0.30001766: {} + capacitor-native-settings@8.1.0(@capacitor/core@7.5.0): + dependencies: + '@capacitor/core': 7.5.0 + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -14430,6 +15473,8 @@ snapshots: damerau-levenshtein@1.0.8: {} + data-uri-to-buffer@4.0.1: {} + data-uri-to-buffer@6.0.2: {} data-view-buffer@1.0.2: @@ -14586,6 +15631,14 @@ snapshots: duplexer@0.1.2: {} + duplexify@4.1.3: + dependencies: + end-of-stream: 1.4.5 + inherits: 2.0.4 + readable-stream: 3.6.2 + stream-shift: 1.0.3 + optional: true + eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -15058,6 +16111,9 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: + optional: true + eventemitter3@5.0.1: {} events-universal@1.0.1: @@ -15172,6 +16228,8 @@ snapshots: ext-list: 2.2.2 sort-keys-length: 1.0.1 + extend@3.0.2: {} + external-editor@3.1.0: dependencies: chardet: 0.7.0 @@ -15188,6 +16246,8 @@ snapshots: transitivePeerDependencies: - supports-color + farmhash-modern@1.1.0: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -15212,14 +16272,34 @@ snapshots: fast-uri@3.1.0: {} + fast-xml-builder@1.2.1: + dependencies: + path-expression-matcher: 1.6.1 + xml-naming: 0.1.0 + optional: true + fast-xml-parser@5.2.5: dependencies: strnum: 2.1.2 + fast-xml-parser@5.9.3: + dependencies: + '@nodable/entities': 2.2.0 + fast-xml-builder: 1.2.1 + is-unsafe: 1.0.1 + path-expression-matcher: 1.6.1 + strnum: 2.4.1 + xml-naming: 0.1.0 + optional: true + fastq@1.19.1: dependencies: reusify: 1.1.0 + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.5 + fb-watchman@2.0.2: dependencies: bser: 2.1.1 @@ -15234,6 +16314,11 @@ snapshots: fecha@4.2.3: {} + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} figures@2.0.0: @@ -15315,6 +16400,56 @@ snapshots: dependencies: semver-regex: 4.0.5 + firebase-admin@14.1.0: + dependencies: + '@fastify/busboy': 3.2.0 + '@firebase/database-compat': 2.1.4 + '@firebase/database-types': 1.0.20 + farmhash-modern: 1.1.0 + fast-deep-equal: 3.1.3 + google-auth-library: 10.9.0 + jsonwebtoken: 9.0.3 + jwks-rsa: 4.1.0 + optionalDependencies: + '@google-cloud/firestore': 8.6.0 + '@google-cloud/storage': 7.21.0 + transitivePeerDependencies: + - encoding + - supports-color + + firebase@12.15.0: + dependencies: + '@firebase/ai': 2.13.1(@firebase/app-types@0.9.5)(@firebase/app@0.15.0) + '@firebase/analytics': 0.10.22(@firebase/app@0.15.0) + '@firebase/analytics-compat': 0.2.28(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) + '@firebase/app': 0.15.0 + '@firebase/app-check': 0.12.0(@firebase/app@0.15.0) + '@firebase/app-check-compat': 0.4.5(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) + '@firebase/app-compat': 0.5.14 + '@firebase/app-types': 0.9.5 + '@firebase/auth': 1.13.3(@firebase/app@0.15.0) + '@firebase/auth-compat': 0.6.8(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0) + '@firebase/data-connect': 0.7.1(@firebase/app@0.15.0) + '@firebase/database': 1.1.3 + '@firebase/database-compat': 2.1.4 + '@firebase/firestore': 4.16.0(@firebase/app@0.15.0) + '@firebase/firestore-compat': 0.4.11(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0) + '@firebase/functions': 0.13.5(@firebase/app@0.15.0) + '@firebase/functions-compat': 0.4.5(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) + '@firebase/installations': 0.6.22(@firebase/app@0.15.0) + '@firebase/installations-compat': 0.2.22(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0) + '@firebase/messaging': 0.13.0(@firebase/app@0.15.0) + '@firebase/messaging-compat': 0.2.27(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) + '@firebase/performance': 0.7.12(@firebase/app@0.15.0) + '@firebase/performance-compat': 0.2.25(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) + '@firebase/remote-config': 0.8.5(@firebase/app@0.15.0) + '@firebase/remote-config-compat': 0.2.26(@firebase/app-compat@0.5.14)(@firebase/app@0.15.0) + '@firebase/storage': 0.14.3(@firebase/app@0.15.0) + '@firebase/storage-compat': 0.4.3(@firebase/app-compat@0.5.14)(@firebase/app-types@0.9.5)(@firebase/app@0.15.0) + '@firebase/util': 1.15.1 + transitivePeerDependencies: + - '@react-native-async-storage/async-storage' + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -15352,6 +16487,16 @@ snapshots: form-data-encoder@2.1.4: {} + form-data@2.5.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + safe-buffer: 5.2.1 + optional: true + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -15360,6 +16505,10 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + formidable@3.5.4: dependencies: '@paralleldrive/cuid2': 2.3.1 @@ -15423,8 +16572,68 @@ snapshots: hasown: 2.0.2 is-callable: 1.2.7 + functional-red-black-tree@1.0.1: + optional: true + functions-have-names@1.2.3: {} + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + gaxios@7.1.3: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + rimraf: 5.0.10 + transitivePeerDependencies: + - supports-color + optional: true + + gaxios@7.1.5: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.1.5 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.3: + dependencies: + gaxios: 7.1.3 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + optional: true + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -15541,6 +16750,65 @@ snapshots: globrex@0.1.2: {} + google-auth-library@10.5.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.5 + gcp-metadata: 8.1.3 + google-logging-utils: 1.1.3 + gtoken: 8.0.0 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + optional: true + + google-auth-library@10.9.0: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.1.5 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + google-gax@5.0.7: + dependencies: + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.8.1 + duplexify: 4.1.3 + google-auth-library: 10.5.0 + google-logging-utils: 1.1.3 + node-fetch: 3.3.2 + object-hash: 3.0.0 + proto3-json-serializer: 3.0.4 + protobufjs: 7.6.5 + retry-request: 8.0.3 + rimraf: 5.0.10 + transitivePeerDependencies: + - supports-color + optional: true + + google-logging-utils@0.0.2: + optional: true + + google-logging-utils@1.1.3: {} + gopd@1.2.0: {} got@13.0.0: @@ -15561,6 +16829,23 @@ snapshots: graphql@16.12.0: {} + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + gtoken@8.0.0: + dependencies: + gaxios: 7.1.5 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + optional: true + gzip-size@6.0.0: dependencies: duplexer: 0.1.2 @@ -15598,6 +16883,11 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + optional: true + headers-polyfill@4.0.3: {} hermes-estree@0.25.1: {} @@ -15606,6 +16896,9 @@ snapshots: dependencies: hermes-estree: 0.25.1 + html-entities@2.6.0: + optional: true + html-escaper@2.0.2: {} http-cache-semantics@4.2.0: {} @@ -15620,6 +16913,17 @@ snapshots: http-link-header@1.1.3: {} + http-parser-js@0.5.10: {} + + http-proxy-agent@5.0.0: + dependencies: + '@tootallnate/once': 2.0.1 + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -15662,6 +16966,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb@7.1.1: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -15894,6 +17200,9 @@ snapshots: is-unicode-supported@0.1.0: {} + is-unsafe@1.0.1: + optional: true + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -16006,7 +17315,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.7 + '@types/node': 25.5.0 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.0 @@ -16131,7 +17440,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.19.7 + '@types/node': 25.5.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -16196,7 +17505,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.19.7 + '@types/node': 25.5.0 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -16346,13 +17655,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.19.27 + '@types/node': 25.5.0 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 22.19.7 + '@types/node': 25.5.0 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -16407,6 +17716,10 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} @@ -16448,7 +17761,7 @@ snapshots: lodash.isstring: 4.0.1 lodash.once: 4.1.1 ms: 2.1.3 - semver: 7.7.3 + semver: 7.7.4 jsx-ast-utils@3.3.5: dependencies: @@ -16463,6 +17776,17 @@ snapshots: ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 + jwks-rsa@4.1.0: + dependencies: + '@types/jsonwebtoken': 9.0.10 + debug: 4.4.3 + jose: 6.1.3 + limiter: 1.1.5 + lru-cache: 11.2.4 + lru-memoizer: 3.0.0 + transitivePeerDependencies: + - supports-color + jws@4.0.1: dependencies: jwa: 2.0.1 @@ -16606,6 +17930,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + limiter@1.1.5: {} + lines-and-columns@1.2.4: {} lint-staged@16.2.7: @@ -16645,6 +17971,10 @@ snapshots: lodash-es@4.17.23: {} + lodash.camelcase@4.3.0: {} + + lodash.clonedeep@4.5.0: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} @@ -16687,6 +18017,8 @@ snapshots: safe-stable-stringify: 2.5.0 triple-beam: 1.4.1 + long@5.3.2: {} + lookup-closest-locale@6.2.0: {} loose-envify@1.4.0: @@ -16707,6 +18039,11 @@ snapshots: lru-cache@7.18.3: {} + lru-memoizer@3.0.0: + dependencies: + lodash.clonedeep: 4.5.0 + lru-cache: 11.2.4 + lucide-react@0.561.0(react@19.2.1): dependencies: react: 19.2.1 @@ -16794,6 +18131,9 @@ snapshots: mime@2.6.0: {} + mime@3.0.0: + optional: true + mimic-fn@1.2.0: {} mimic-fn@2.1.0: {} @@ -16977,6 +18317,8 @@ snapshots: node-abort-controller@3.1.1: {} + node-domexception@1.0.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.17.21 @@ -16985,6 +18327,12 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-int64@0.4.0: {} node-releases@2.0.27: {} @@ -17219,6 +18567,9 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.6.1: + optional: true + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -17402,6 +18753,25 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proto3-json-serializer@3.0.4: + dependencies: + protobufjs: 7.6.5 + optional: true + + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 25.5.0 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -17476,6 +18846,8 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 + re2js@0.4.3: {} + react-day-picker@9.12.0(react@19.2.1): dependencies: '@date-fns/tz': 1.4.1 @@ -17692,6 +19064,27 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry-request@7.0.2: + dependencies: + '@types/request': 2.48.13 + extend: 3.0.2 + teeny-request: 9.0.0 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + + retry-request@8.0.3: + dependencies: + extend: 3.0.2 + teeny-request: 10.1.3 + transitivePeerDependencies: + - supports-color + optional: true + + retry@0.13.1: + optional: true + rettime@0.7.0: {} reusify@1.1.0: {} @@ -17706,6 +19099,11 @@ snapshots: dependencies: glob: 7.2.3 + rimraf@5.0.10: + dependencies: + glob: 10.5.0 + optional: true + rimraf@6.1.3: dependencies: glob: 13.0.6 @@ -18167,6 +19565,14 @@ snapshots: - react-dom - utf-8-validate + stream-events@1.0.5: + dependencies: + stubs: 3.0.0 + optional: true + + stream-shift@1.0.3: + optional: true + streamsearch@1.1.0: {} streamx@2.23.0: @@ -18306,10 +19712,18 @@ snapshots: strnum@2.1.2: {} + strnum@2.4.1: + dependencies: + anynum: 1.0.1 + optional: true + strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 + stubs@3.0.0: + optional: true + styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.1): dependencies: client-only: 0.0.1 @@ -18413,6 +19827,28 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + teeny-request@10.1.3: + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + stream-events: 1.0.5 + transitivePeerDependencies: + - supports-color + optional: true + + teeny-request@9.0.0: + dependencies: + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0 + stream-events: 1.0.5 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + optional: true + teex@1.0.1: dependencies: streamx: 2.23.0 @@ -18468,7 +19904,7 @@ snapshots: third-party-web@0.26.7: {} - third-party-web@0.29.0: {} + third-party-web@0.29.2: {} through2@4.0.2: dependencies: @@ -18986,6 +20422,10 @@ snapshots: dependencies: defaults: 1.0.4 + web-streams-polyfill@3.3.3: {} + + web-vitals@4.2.4: {} + webdriver-bidi-protocol@0.4.1: {} webidl-conversions@3.0.1: {} @@ -19081,6 +20521,14 @@ snapshots: - esbuild - uglify-js + websocket-driver@0.7.5: + dependencies: + http-parser-js: 0.5.10 + safe-buffer: 5.2.1 + websocket-extensions: 0.1.4 + + websocket-extensions@0.1.4: {} + whatwg-fetch@3.6.20: {} whatwg-url@5.0.0: @@ -19228,6 +20676,9 @@ snapshots: xdg-basedir@4.0.0: {} + xml-naming@0.1.0: + optional: true + xml2js@0.6.2: dependencies: sax: 1.5.0