diff --git a/.env.example b/.env.example index 7130accd..bf8470b0 100644 --- a/.env.example +++ b/.env.example @@ -37,3 +37,11 @@ PRIVATE_LIBELLUS_API_TOKEN=replace-me # --- Analytics --- PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX + +# --- Firebase Cloud Messaging (web push; $env/static/public) --- +PUBLIC_FIREBASE_API_KEY=replace-me +PUBLIC_FIREBASE_AUTH_DOMAIN=example.firebaseapp.com +PUBLIC_FIREBASE_PROJECT_ID=replace-me +PUBLIC_FIREBASE_MESSAGING_SENDER_ID=000000000000 +PUBLIC_FIREBASE_APP_ID=1:000000000000:web:replace-me +PUBLIC_FIREBASE_VAPID_KEY=replace-me diff --git a/messages/en.json b/messages/en.json index ea2d1a55..645d4b54 100644 --- a/messages/en.json +++ b/messages/en.json @@ -152,6 +152,22 @@ "line_recipient_placeholder": "Select users to notify on LINE", "line_recipient_not_linked": "LINE not linked", "line_recipient_preview_count": "{count} LINE user(s)", + "push_card_title": "Push notifications", + "push_card_subtitle": "Receive CropWatch alerts as push notifications on this device.", + "push_enabled_notice": "Push notifications are enabled on this device.", + "push_disabled_notice": "Push notifications are not enabled on this device yet. Enable them to receive alerts even when the app is closed.", + "push_unsupported_notice": "This browser does not support push notifications.", + "push_ios_install_notice": "On iPhone or iPad, first add CropWatch to your Home Screen (Share → Add to Home Screen), then open it from there to enable push notifications.", + "push_denied_notice": "Notifications are blocked for this site. Allow notifications in your browser settings, then try again.", + "push_enable_action": "Enable push notifications", + "push_disable_action": "Disable on this device", + "push_enabled_toast": "Push notifications enabled.", + "push_disabled_toast": "Push notifications disabled on this device.", + "push_enable_failed": "Could not enable push notifications. Please try again.", + "push_recipient_label": "Push recipients", + "push_recipient_placeholder": "Select users to notify by push", + "push_recipient_not_enabled": "Push not set up", + "push_recipient_preview_count": "{count} push recipient(s)", "settings_regional_title": "Regional", "settings_regional_subtitle": "Language, timezone, and calendar notation used across the app.", "settings_timezone_label": "Home timezone", diff --git a/messages/ja.json b/messages/ja.json index d8d1e8dd..64717171 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -152,6 +152,22 @@ "line_recipient_placeholder": "LINEで通知するユーザーを選択", "line_recipient_not_linked": "LINE未連携", "line_recipient_preview_count": "LINEユーザー{count}名", + "push_card_title": "プッシュ通知", + "push_card_subtitle": "このデバイスでCropWatchアラートをプッシュ通知として受け取れます。", + "push_enabled_notice": "このデバイスでプッシュ通知が有効になっています。", + "push_disabled_notice": "このデバイスではプッシュ通知がまだ有効になっていません。有効にすると、アプリを閉じていてもアラートを受け取れます。", + "push_unsupported_notice": "このブラウザはプッシュ通知に対応していません。", + "push_ios_install_notice": "iPhone・iPadでは、まずCropWatchをホーム画面に追加(共有 → ホーム画面に追加)し、そこから開いてプッシュ通知を有効にしてください。", + "push_denied_notice": "このサイトの通知がブロックされています。ブラウザの設定で通知を許可してから、もう一度お試しください。", + "push_enable_action": "プッシュ通知を有効にする", + "push_disable_action": "このデバイスで無効にする", + "push_enabled_toast": "プッシュ通知を有効にしました。", + "push_disabled_toast": "このデバイスのプッシュ通知を無効にしました。", + "push_enable_failed": "プッシュ通知を有効にできませんでした。もう一度お試しください。", + "push_recipient_label": "プッシュ通知先", + "push_recipient_placeholder": "プッシュ通知するユーザーを選択", + "push_recipient_not_enabled": "プッシュ未設定", + "push_recipient_preview_count": "プッシュ通知先{count}名", "settings_regional_title": "地域設定", "settings_regional_subtitle": "アプリ全体で使用される言語、タイムゾーン、カレンダー表記です。", "settings_timezone_label": "ホームタイムゾーン", diff --git a/package.json b/package.json index 6567f028..d8879f4e 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "dependencies": { "@cropwatchdevelopment/cwui": "0.1.113", "@supabase/supabase-js": "^2.108.2", + "firebase": "^12.18.0", "jwt-decode": "^4.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06b0ed9d..e6935cc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: '@supabase/supabase-js': specifier: ^2.108.2 version: 2.108.2 + firebase: + specifier: ^12.18.0 + version: 12.18.0 jwt-decode: specifier: ^4.0.0 version: 4.0.0 @@ -324,6 +327,243 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@firebase/ai@2.15.0': + resolution: {integrity: sha512-Aj7TbFdAIWZdkX8JfdDStERpR35g6WNs+7XhNPtFLFOizUotj6k4N/D8HJkR45165HhnMJBe4hjOeeaxnnn56Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-types': 0.x + + '@firebase/analytics-compat@0.2.30': + resolution: {integrity: sha512-uVZEKlLaW4AHAhv8zoN9cTmveX6v86AVqcJ4LCaRCMTChTH8/NsjbisTq1lpBfWCLWS1spqwSHB4vol/YSCdMA==} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + + '@firebase/analytics-types@0.8.5': + resolution: {integrity: sha512-kdnooE7Bis2jEnsqcerRwn/UQpH5D3uvHpku7OdUM9TJN3omlu6iYtbyAQ6XkAJRgJtO0aNf9OOkW8J6D59oCA==} + + '@firebase/analytics@0.10.24': + resolution: {integrity: sha512-OfIAcIIwoqXjBzS+DnUQpOZ7i3ePaNFg83KPpDWjBZUKJmqMwzzAtLJqmKZOkQnW8im6vFR6f2I3M/9hxVd+QA==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-check-compat@0.4.7': + resolution: {integrity: sha512-fBb/xSMyIKv1nDmccfzz3IAGBzx/cQOxmZai66rJzifb1hMMkztf824Xx7LhpTUe7HNUbXwY90IvJ66fi8q6yg==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + + '@firebase/app-check-interop-types@0.3.5': + resolution: {integrity: sha512-qId34pVZ2CXTmtu4ofW5leiI93DvnOvIcr/5GT7MZPw5WXAi6nZoU+g9cNRgl7K90UJSbK0cXxten4meYMPyZg==} + + '@firebase/app-check-types@0.5.5': + resolution: {integrity: sha512-+DF4gzFrlwGFyky38o4T/YN/r51l70yCtJ2HTkwmRj8FCMwPjm4hLH4fQbj6BUvzkiFqliBkitFsRpf1/YZkWA==} + + '@firebase/app-check@0.13.1': + resolution: {integrity: sha512-l8y3dmnhodXks/APAwx4tWqRl3tk8b9874KF1FJyKg1DIU+kMD0l52iz7S9/MW+eK8k6cjJg0G0iJ5KQAsQpow==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/app-compat@0.5.17': + resolution: {integrity: sha512-5GdJWobqs6jbNYOnaQiSa4Ng8gnFerhqr7nY+v5jlnT7o9QbEeIZRLPQpnLe0TxYSWfRV2lAMbyR0kbXeIos9Q==} + engines: {node: '>=20.0.0'} + + '@firebase/app-types@0.9.6': + resolution: {integrity: sha512-yPLahy7Esfu2w/yme3msVK4xTkDXQqq6szfQn8yVOQpCKiT5GVFjqNpLbuz6NkX0WuTwUidkmPewW3r4xkvpeg==} + + '@firebase/app@0.16.1': + resolution: {integrity: sha512-tjUEorFyKrurH7PbLWv9zDHRuU4mLefgD/yY38D584ziorIRlQz/PVjx4c/SCGyCuUlmyzt72mK+Lh+COAywNQ==} + engines: {node: '>=20.0.0'} + + '@firebase/auth-compat@0.6.10': + resolution: {integrity: sha512-Bvklg2nL7BrBFCAqdsleW8fse9Jh0fARqJPlJmA40uermfWx1bJiO7dnKyZN3J39d2TFVd4VJXtg3i6Wg92/YQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + + '@firebase/auth-interop-types@0.2.6': + resolution: {integrity: sha512-FgwZqDrBqgK0BHI70QTv1v/5wmuDF9f8fsJFvKSxoRlK2PPfSQtZAk2jhXu4pe9BZzFi/e4UYusU/bEQHdf6Qg==} + + '@firebase/auth-types@0.13.2': + resolution: {integrity: sha512-OU+miuoSxIWYN7GT291d2ylVJBL3k/OePo7/JDSoQtkFQoEiGBc4AHuMjj/seqFIsHrqnjrAfRCk4pfufoJolQ==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/auth@1.13.5': + resolution: {integrity: sha512-1AXoBJqBVD8WL8FZYo3S2GmJF9YUoom6Y6ngMxOSkzzhW5sT83pLchb6TGFgxes91dfXx8s/VYc5VrLDNqpLog==} + 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.5': + resolution: {integrity: sha512-vuFDcL91Q+2ZuBJkyOh86T4q0B4ffNTDjc/A38tybO56odQABxRTFLTIowCWqAKeIcgo37GowWMVgFF73gD8Qw==} + engines: {node: '>=20.0.0'} + + '@firebase/data-connect@0.7.4': + resolution: {integrity: sha512-su1aGWlzhxb+xtggCUSsufJn1FDa06SBDK71y+fpQ+g2zMhMExik6FUv/odkKzOF/xfWVjabCbWBcjJY3MceDA==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/database-compat@2.1.7': + resolution: {integrity: sha512-lBq9sJm8MnJINKJnkAKSOj2MbC66xGoSVCcGkPtstl+lkoKEBtD46qHLbuDgW/WbLPoKVm17RIN8BxHj+Z2F2w==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + peerDependenciesMeta: + '@firebase/app': + optional: true + '@firebase/app-compat': + optional: true + + '@firebase/database-types@1.0.22': + resolution: {integrity: sha512-YAZNXsjY9EQQ+pKw/3ax8n5FgolHC7Qew7EY5RceYdl0R2ZP+kCv1O0DkKlcceue8uQCOreHCNN7PWVFpY1Nug==} + + '@firebase/database@1.1.5': + resolution: {integrity: sha512-/JGpvszLoNXNgzilRXocigGfFF4hbcPA9wN1i1kjJx6oKkXgkHteZYl3lQs1lJX3ETDf6bv7zI15lPMVUp4sAQ==} + engines: {node: '>=20.0.0'} + + '@firebase/firestore-compat@0.4.13': + resolution: {integrity: sha512-l9dCewxMzzLOIhcwTjERCKxrWOn1kZ9JvwOQq9zZNq4I/Nbvb/B9njT230V61uvQ9qZ3/qpUG758D8DDTEFXnw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + + '@firebase/firestore-types@3.0.5': + resolution: {integrity: sha512-dbdMAQkMd5dwWc48eupz/Y6/E9ruat3+gY5lhVKscvvT/HnDBEEMzJW38zdKhgdnglZHGk2vUsJMOHAphMHGMA==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/firestore@4.17.1': + resolution: {integrity: sha512-8lqPNf2w10CtYG+tayVjZO1pSyQpnhztQRudeD109VtXDzNbASTaYdO43sj5PMsDcWq0aOYY3RmJOUlXu9++jw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/functions-compat@0.5.0': + resolution: {integrity: sha512-T3BDIToESZUHt7438wyKYMkRjKg3m1xAIux5fVpNvTRQlDOFLukWniQWBDhJ2znpU9kxMTR6+e31TVo8P15PZw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + + '@firebase/functions-types@0.6.5': + resolution: {integrity: sha512-Zc0pURjthHXzSj54ZivCkzKDSV1r/wIpnmHdhq82q2yFFPVoncG/ZJjnVMiANvQfeww/QnElhzODpfwUucVwdA==} + + '@firebase/functions@0.14.0': + resolution: {integrity: sha512-DhuYFr0eMhp+s/PNEk6SiMsYkc00+XVOUeKbrl8MOzQNZI3SKQpqoDR1d+keNDAutwmHRWTUAdXBoVPD+xxVUw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/installations-compat@0.2.24': + resolution: {integrity: sha512-8M5nlcWwYt881x83COP2odq5vgf+NgwJh+RMd4LRSv8JI1pxwDfgrDJOERrjTgfC9J5Z0vnQX4b1pdN914e0Zw==} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + + '@firebase/installations-types@0.5.5': + resolution: {integrity: sha512-e9UYcju3puDl1vdrcKIi5dExzHLameOT/Tc61Q48PYwxtsM1NzZh/ikGbdBQTsbRgg0EMZqdPr0/m5ODUBobrg==} + peerDependencies: + '@firebase/app-types': 0.x + + '@firebase/installations@0.6.24': + resolution: {integrity: sha512-Ui52ey8wHoWqkBbXRKJEKYWylI0JZogZmoLS+o8Anh1bxWtK27ZYjLla1UJAAdC5DCKLJ2qAp0VpJOjg8VOX1g==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/logger@0.5.2': + resolution: {integrity: sha512-J2VO4NFTc0xQFrxV1B/lm5balicm9cwuX2acR9Yn41fN8KgUeQFo+VJV222IqW2FPSXKDu9uo5WdQFWf9TPbYg==} + engines: {node: '>=20.0.0'} + + '@firebase/messaging-compat@0.2.29': + resolution: {integrity: sha512-8Twe4CeYvAx8AzjBxyyQFKzinaMGGt13hPQBqaARQ2QZjagrEaqSVBe+Zy6F4L/vT8DV168yRgRu7W/RK7p+4w==} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + + '@firebase/messaging-interop-types@0.2.6': + resolution: {integrity: sha512-MVzvkKe2V4H2dHu5oOxRfeKQcfTwWmCgnzsC4V1q3ixun5iiL8riCZ2qI35rDhCL4glPGiu0jHxDbpVNuTKfow==} + + '@firebase/messaging@0.13.2': + resolution: {integrity: sha512-KcZoqUu2ih4sLH91dW9tmyHjCR0IQyNzSLZunX5uq7cLeImXb4uO2I0wq5FACduO2I+FoTeWa2BtUv7Jpowqdw==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/performance-compat@0.2.27': + resolution: {integrity: sha512-O/ozTf/EbChN94Pk7bd1eUC0PAC36726AwsaiJyC0bZzSBWfLGSWASGPH5pebUWXQPJtGxIJYRkkbX5l0Qa+Hw==} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + + '@firebase/performance-types@0.2.5': + resolution: {integrity: sha512-PRzOgB+/M+6AKlEkY8a9xy5Ff5SfZPIh4iShXhn2WAcL/euSbK7bdLqWysHduunNJhGFsXa22Ag3ixqL6rQtYg==} + + '@firebase/performance@0.7.14': + resolution: {integrity: sha512-9PH1XEZVHErxGdbXluvGz4Uyjw4W955H8v7Mv9rHIybRrg5F2Ac2tvf5+mSf5EtnxWNmxrD2WLnvMFaZP4sMrQ==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/remote-config-compat@0.2.29': + resolution: {integrity: sha512-mY7JtTISK6F4g4T0x3kVepr5cp0NXL7f5yjIUXXGaTrg4qUlFBWRbENaHaqZ78dwmNcLm24h/yPsOVRlDXEXhA==} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + + '@firebase/remote-config-types@0.5.2': + resolution: {integrity: sha512-i8k1omVfoAnaT1ZPv2FFjxMZrATYsO/GnFgHEj5+7fAJLzi8wLnGGv1WK06oEAD6ltrWXSO1HzeNyajn/NjMWw==} + + '@firebase/remote-config@0.9.2': + resolution: {integrity: sha512-Rii93DkXjM+RE/ytHdYa7EIiQLJbdBr+W8EEiqd/vbLCOC+1FdkDuRiumJBp6t3yewHGbdqbpjB3fk3z0cZ+8g==} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/storage-compat@0.4.5': + resolution: {integrity: sha512-vO0tFPxXbKDKdlTu8tYT08S9t9ezUTJYEdLCULpWxWq2aq6zojwN1QmAB+1a50AI/g47GlWUJn1XPncm/PDZsw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + '@firebase/app-compat': 0.x + + '@firebase/storage-types@0.8.5': + resolution: {integrity: sha512-GEDs5P+rNUfNcS+wxIdOLAHficife2YXtvTJnyi3ssrX11AAtBg+nDUdbYh8vuBzWehVQZPmztGUUnud4L+4yg==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + + '@firebase/storage@0.14.5': + resolution: {integrity: sha512-r2tozN/BlEewLi70tJNUQPzWwbex9GM7NgXZsamHKQrjKEfcR0i4jGgHBKAKA4hbwiPE9eNMb3hcxWnDpeJZwg==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@firebase/app': 0.x + + '@firebase/util@1.15.3': + resolution: {integrity: sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==} + engines: {node: '>=20.0.0'} + + '@firebase/webchannel-wrapper@1.0.7': + resolution: {integrity: sha512-phBFwieDLvkZGYN9CE9ZFNEIoBVksprzsnCzQejCmCHtgwCXReeuRpoEGN9C4EbhONztv8NRV1tau6Rb9pONwQ==} + + '@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 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -404,6 +644,33 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@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==} + '@rollup/pluginutils@5.4.0': resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} engines: {node: '>=14.0.0'} @@ -447,79 +714,66 @@ packages: resolution: {integrity: sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.62.0': resolution: {integrity: sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.62.0': resolution: {integrity: sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.62.0': resolution: {integrity: sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.62.0': resolution: {integrity: sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.62.0': resolution: {integrity: sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.62.0': resolution: {integrity: sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.62.0': resolution: {integrity: sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.62.0': resolution: {integrity: sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.62.0': resolution: {integrity: sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.62.0': resolution: {integrity: sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.62.0': resolution: {integrity: sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.62.0': resolution: {integrity: sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.62.0': resolution: {integrity: sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==} @@ -579,25 +833,21 @@ packages: resolution: {integrity: sha512-5se1bYRIzivL+ehImnwFjBpzKZ+AwLS6/cBg6lvJOJdWatuaK9Edu6wOZAmPjVMy4vZRJ27tdL+3F7N+Vk64AA==} cpu: [arm64] os: [linux] - libc: [musl] '@supabase/cli-linux-arm64@2.107.0': resolution: {integrity: sha512-an0dsOhPcLQXZ0sFEBm6NU1HFUjXCStHxetfJvgt4u7SiH/EyDMLfDvV7W9ef56bgG+3hoWHtih2Kplj3cecQA==} cpu: [arm64] os: [linux] - libc: [glibc] '@supabase/cli-linux-x64-musl@2.107.0': resolution: {integrity: sha512-gXDzvDsmmJw9HbeVRFD6xboGwbu/cpBp84ZESEcXKyB8Onvs3igWKwoXMofx+ppFM1EaZb/flTwJ77b19EMD1A==} cpu: [x64] os: [linux] - libc: [musl] '@supabase/cli-linux-x64@2.107.0': resolution: {integrity: sha512-8ttnpfBFEXk0JFmH6gw8ZCVcMa/UNH1sD6iG9PQLbK2eXdZ6kDIzGs5g+CEvxZ3j6HxEIYn9h9rfxoqGoBUgwA==} cpu: [x64] os: [linux] - libc: [glibc] '@supabase/cli-windows-arm64@2.107.0': resolution: {integrity: sha512-V/4q5dbDgBQXhDAJaHamTm9u/kY2UJ4nNibVKOYMaLh9q7H74b8unteJ+rYHSwWZy0EIT8DTDryGP6xatKQicw==} @@ -721,28 +971,24 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.3.1': resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.3.1': resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.3.1': resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.3.1': resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} @@ -934,6 +1180,10 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -996,6 +1246,10 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1076,6 +1330,9 @@ packages: devalue@5.8.2: resolution: {integrity: sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + enhanced-resolve@5.21.6: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} @@ -1088,6 +1345,10 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1127,6 +1388,7 @@ packages: eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: jiti: '*' @@ -1189,6 +1451,10 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + faye-websocket@0.11.4: + resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} + engines: {node: '>=0.8.0'} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1209,6 +1475,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + firebase@12.18.0: + resolution: {integrity: sha512-XaL6tlE5Xd20ZDhckqOMIw+JJTET+wTdeZPxQ7ihc42oxRb7kWUyn/j1LO5V9dH1xq8Rv5R71Pv1fBCdIkt9Rw==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -1226,6 +1495,10 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -1253,6 +1526,9 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + http-parser-js@0.5.10: + resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -1265,6 +1541,9 @@ packages: resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} engines: {node: '>=20.0.0'} + idb@7.1.1: + resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1285,6 +1564,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1377,28 +1660,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1427,9 +1706,15 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@11.5.1: resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} @@ -1647,14 +1932,26 @@ packages: engines: {node: '>=14'} hasBin: true + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + re2js@2.8.6: + resolution: {integrity: sha512-xLgQil4kIUCrAzVk9fRSkxkFNwmygLFjVxXrLc65aE1F0+Zsb8rxumFBy4XKyvgMCTL6kilDq3EZ0piE2dP/Dg==} + engines: {node: '>=18.0.0'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1672,6 +1969,9 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + semver@7.8.4: resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==} engines: {node: '>=10'} @@ -1710,6 +2010,14 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1925,12 +2233,23 @@ packages: jsdom: optional: true + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + 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-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -1948,6 +2267,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + ws@8.21.0: resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} @@ -1960,6 +2283,10 @@ packages: utf-8-validate: optional: true + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -1968,6 +2295,14 @@ packages: resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} engines: {node: '>= 6'} + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2117,6 +2452,336 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@firebase/ai@2.15.0(@firebase/app-types@0.9.6)(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-check-interop-types': 0.3.5 + '@firebase/app-types': 0.9.6 + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/analytics-compat@0.2.30(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1)': + dependencies: + '@firebase/analytics': 0.10.24(@firebase/app@0.16.1) + '@firebase/analytics-types': 0.8.5 + '@firebase/app': 0.16.1 + '@firebase/app-compat': 0.5.17 + '@firebase/component': 0.7.5 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/analytics-types@0.8.5': {} + + '@firebase/analytics@0.10.24(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/component': 0.7.5 + '@firebase/installations': 0.6.24(@firebase/app@0.16.1) + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/app-check-compat@0.4.7(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-check': 0.13.1(@firebase/app@0.16.1) + '@firebase/app-check-types': 0.5.5 + '@firebase/app-compat': 0.5.17 + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/app-check-interop-types@0.3.5': {} + + '@firebase/app-check-types@0.5.5': {} + + '@firebase/app-check@0.13.1(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/app-compat@0.5.17': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/app-types@0.9.6': + dependencies: + '@firebase/logger': 0.5.2 + + '@firebase/app@0.16.1': + dependencies: + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/auth-compat@0.6.10(@firebase/app-compat@0.5.17)(@firebase/app-types@0.9.6)(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-compat': 0.5.17 + '@firebase/auth': 1.13.5(@firebase/app@0.16.1) + '@firebase/auth-types': 0.13.2(@firebase/app-types@0.9.6)(@firebase/util@1.15.3) + '@firebase/component': 0.7.5 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app-types' + - '@react-native-async-storage/async-storage' + + '@firebase/auth-interop-types@0.2.6': {} + + '@firebase/auth-types@0.13.2(@firebase/app-types@0.9.6)(@firebase/util@1.15.3)': + dependencies: + '@firebase/app-types': 0.9.6 + '@firebase/util': 1.15.3 + + '@firebase/auth@1.13.5(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/component@0.7.5': + dependencies: + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/data-connect@0.7.4(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/auth-interop-types': 0.2.6 + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/database-compat@2.1.7(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1)': + dependencies: + '@firebase/component': 0.7.5 + '@firebase/database': 1.1.5 + '@firebase/database-types': 1.0.22 + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + optionalDependencies: + '@firebase/app': 0.16.1 + '@firebase/app-compat': 0.5.17 + + '@firebase/database-types@1.0.22': + dependencies: + '@firebase/app-types': 0.9.6 + '@firebase/util': 1.15.3 + + '@firebase/database@1.1.5': + dependencies: + '@firebase/app-check-interop-types': 0.3.5 + '@firebase/auth-interop-types': 0.2.6 + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + faye-websocket: 0.11.4 + tslib: 2.8.1 + + '@firebase/firestore-compat@0.4.13(@firebase/app-compat@0.5.17)(@firebase/app-types@0.9.6)(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-compat': 0.5.17 + '@firebase/component': 0.7.5 + '@firebase/firestore': 4.17.1(@firebase/app@0.16.1) + '@firebase/firestore-types': 3.0.5(@firebase/app-types@0.9.6)(@firebase/util@1.15.3) + '@firebase/util': 1.15.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app-types' + + '@firebase/firestore-types@3.0.5(@firebase/app-types@0.9.6)(@firebase/util@1.15.3)': + dependencies: + '@firebase/app-types': 0.9.6 + '@firebase/util': 1.15.3 + + '@firebase/firestore@4.17.1(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + '@firebase/webchannel-wrapper': 1.0.7 + '@grpc/grpc-js': 1.9.16 + '@grpc/proto-loader': 0.7.15 + re2js: 2.8.6 + tslib: 2.8.1 + + '@firebase/functions-compat@0.5.0(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-compat': 0.5.17 + '@firebase/component': 0.7.5 + '@firebase/functions': 0.14.0(@firebase/app@0.16.1) + '@firebase/functions-types': 0.6.5 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/functions-types@0.6.5': {} + + '@firebase/functions@0.14.0(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-check-interop-types': 0.3.5 + '@firebase/auth-interop-types': 0.2.6 + '@firebase/component': 0.7.5 + '@firebase/messaging-interop-types': 0.2.6 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/installations-compat@0.2.24(@firebase/app-compat@0.5.17)(@firebase/app-types@0.9.6)(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-compat': 0.5.17 + '@firebase/component': 0.7.5 + '@firebase/installations': 0.6.24(@firebase/app@0.16.1) + '@firebase/installations-types': 0.5.5(@firebase/app-types@0.9.6) + '@firebase/util': 1.15.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app-types' + + '@firebase/installations-types@0.5.5(@firebase/app-types@0.9.6)': + dependencies: + '@firebase/app-types': 0.9.6 + + '@firebase/installations@0.6.24(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/component': 0.7.5 + '@firebase/util': 1.15.3 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/logger@0.5.2': + dependencies: + tslib: 2.8.1 + + '@firebase/messaging-compat@0.2.29(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-compat': 0.5.17 + '@firebase/component': 0.7.5 + '@firebase/messaging': 0.13.2(@firebase/app@0.16.1) + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/messaging-interop-types@0.2.6': {} + + '@firebase/messaging@0.13.2(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/component': 0.7.5 + '@firebase/installations': 0.6.24(@firebase/app@0.16.1) + '@firebase/messaging-interop-types': 0.2.6 + '@firebase/util': 1.15.3 + idb: 7.1.1 + tslib: 2.8.1 + + '@firebase/performance-compat@0.2.27(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-compat': 0.5.17 + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/performance': 0.7.14(@firebase/app@0.16.1) + '@firebase/performance-types': 0.2.5 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/performance-types@0.2.5': {} + + '@firebase/performance@0.7.14(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/component': 0.7.5 + '@firebase/installations': 0.6.24(@firebase/app@0.16.1) + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + web-vitals: 4.2.4 + + '@firebase/remote-config-compat@0.2.29(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-compat': 0.5.17 + '@firebase/component': 0.7.5 + '@firebase/logger': 0.5.2 + '@firebase/remote-config': 0.9.2(@firebase/app@0.16.1) + '@firebase/remote-config-types': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/remote-config-types@0.5.2': {} + + '@firebase/remote-config@0.9.2(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/component': 0.7.5 + '@firebase/installations': 0.6.24(@firebase/app@0.16.1) + '@firebase/logger': 0.5.2 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/storage-compat@0.4.5(@firebase/app-compat@0.5.17)(@firebase/app-types@0.9.6)(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/app-compat': 0.5.17 + '@firebase/component': 0.7.5 + '@firebase/storage': 0.14.5(@firebase/app@0.16.1) + '@firebase/storage-types': 0.8.5(@firebase/app-types@0.9.6)(@firebase/util@1.15.3) + '@firebase/util': 1.15.3 + tslib: 2.8.1 + transitivePeerDependencies: + - '@firebase/app-types' + + '@firebase/storage-types@0.8.5(@firebase/app-types@0.9.6)(@firebase/util@1.15.3)': + dependencies: + '@firebase/app-types': 0.9.6 + '@firebase/util': 1.15.3 + + '@firebase/storage@0.14.5(@firebase/app@0.16.1)': + dependencies: + '@firebase/app': 0.16.1 + '@firebase/component': 0.7.5 + '@firebase/util': 1.15.3 + tslib: 2.8.1 + + '@firebase/util@1.15.3': + dependencies: + tslib: 2.8.1 + + '@firebase/webchannel-wrapper@1.0.7': {} + + '@grpc/grpc-js@1.9.16': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@types/node': 22.19.21 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.5 + yargs: 17.7.3 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -2220,6 +2885,26 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@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': {} + '@rollup/pluginutils@5.4.0(rollup@4.62.0)': dependencies: '@types/estree': 1.0.9 @@ -2716,6 +3401,8 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@5.0.1: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -2764,6 +3451,12 @@ snapshots: chownr@3.0.0: {} + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clsx@2.1.1: {} color-convert@2.0.1: @@ -2813,6 +3506,8 @@ snapshots: devalue@5.8.2: {} + emoji-regex@8.0.0: {} + enhanced-resolve@5.21.6: dependencies: graceful-fs: 4.2.11 @@ -2849,6 +3544,8 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)): @@ -2967,6 +3664,10 @@ snapshots: fast-levenshtein@2.0.6: {} + faye-websocket@0.11.4: + dependencies: + websocket-driver: 0.7.5 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -2982,6 +3683,39 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + firebase@12.18.0: + dependencies: + '@firebase/ai': 2.15.0(@firebase/app-types@0.9.6)(@firebase/app@0.16.1) + '@firebase/analytics': 0.10.24(@firebase/app@0.16.1) + '@firebase/analytics-compat': 0.2.30(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1) + '@firebase/app': 0.16.1 + '@firebase/app-check': 0.13.1(@firebase/app@0.16.1) + '@firebase/app-check-compat': 0.4.7(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1) + '@firebase/app-compat': 0.5.17 + '@firebase/app-types': 0.9.6 + '@firebase/auth': 1.13.5(@firebase/app@0.16.1) + '@firebase/auth-compat': 0.6.10(@firebase/app-compat@0.5.17)(@firebase/app-types@0.9.6)(@firebase/app@0.16.1) + '@firebase/data-connect': 0.7.4(@firebase/app@0.16.1) + '@firebase/database': 1.1.5 + '@firebase/database-compat': 2.1.7(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1) + '@firebase/firestore': 4.17.1(@firebase/app@0.16.1) + '@firebase/firestore-compat': 0.4.13(@firebase/app-compat@0.5.17)(@firebase/app-types@0.9.6)(@firebase/app@0.16.1) + '@firebase/functions': 0.14.0(@firebase/app@0.16.1) + '@firebase/functions-compat': 0.5.0(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1) + '@firebase/installations': 0.6.24(@firebase/app@0.16.1) + '@firebase/installations-compat': 0.2.24(@firebase/app-compat@0.5.17)(@firebase/app-types@0.9.6)(@firebase/app@0.16.1) + '@firebase/messaging': 0.13.2(@firebase/app@0.16.1) + '@firebase/messaging-compat': 0.2.29(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1) + '@firebase/performance': 0.7.14(@firebase/app@0.16.1) + '@firebase/performance-compat': 0.2.27(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1) + '@firebase/remote-config': 0.9.2(@firebase/app@0.16.1) + '@firebase/remote-config-compat': 0.2.29(@firebase/app-compat@0.5.17)(@firebase/app@0.16.1) + '@firebase/storage': 0.14.5(@firebase/app@0.16.1) + '@firebase/storage-compat': 0.4.5(@firebase/app-compat@0.5.17)(@firebase/app-types@0.9.6)(@firebase/app@0.16.1) + '@firebase/util': 1.15.3 + transitivePeerDependencies: + - '@react-native-async-storage/async-storage' + flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -2995,6 +3729,8 @@ snapshots: fsevents@2.3.3: optional: true + get-caller-file@2.0.5: {} + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -3015,6 +3751,8 @@ snapshots: has-flag@4.0.0: {} + http-parser-js@0.5.10: {} + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -3026,6 +3764,8 @@ snapshots: iceberg-js@0.8.1: {} + idb@7.1.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -3039,6 +3779,8 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3139,8 +3881,12 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.camelcase@4.3.0: {} + lodash.merge@4.6.2: {} + long@5.3.2: {} + lru-cache@11.5.1: {} magic-string@0.30.21: @@ -3270,10 +4016,28 @@ snapshots: prettier@3.8.4: {} + 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': 22.19.21 + long: 5.3.2 + punycode@2.3.1: {} + re2js@2.8.6: {} + readdirp@4.1.2: {} + require-directory@2.1.1: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -3313,6 +4077,8 @@ snapshots: dependencies: mri: 1.2.0 + safe-buffer@5.2.1: {} + semver@7.8.4: {} set-cookie-parser@3.1.2: {} @@ -3342,6 +4108,16 @@ snapshots: std-env@4.1.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + strip-json-comments@3.1.1: {} supabase@2.107.0: @@ -3534,10 +4310,20 @@ snapshots: transitivePeerDependencies: - msw + web-vitals@4.2.4: {} + webidl-conversions@3.0.1: {} webpack-virtual-modules@0.6.2: {} + 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-url@5.0.0: dependencies: tr46: 0.0.3 @@ -3554,12 +4340,32 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + ws@8.21.0: {} + y18n@5.0.8: {} + yallist@5.0.0: {} yaml@1.10.3: {} + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} zimmerframe@1.1.4: {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e8d72a50..4718c229 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -11,7 +11,8 @@ const PUBLIC_PATHS = new Set([ '/manifest.webmanifest', '/offline', '/offline/', - '/service-worker.js' + '/service-worker.js', + '/push-sw.js' ]); function getAuthRedirectTarget(event: RequestEvent): string { diff --git a/src/lib/api/api.dtos.ts b/src/lib/api/api.dtos.ts index ba0c0310..308e1bfc 100644 --- a/src/lib/api/api.dtos.ts +++ b/src/lib/api/api.dtos.ts @@ -46,6 +46,24 @@ export interface LineRecipientCandidateDto { lineLinked: boolean; } +export interface PushRecipientCandidateDto { + userId: string; + displayName: string; + pushEnabled: boolean; +} + +export interface PushTokenDto { + token: string; + deviceLabel: string | null; + createdAt: string | null; + lastSeenAt: string | null; +} + +export interface RegisterPushTokenRequest { + token: string; + deviceLabel?: string | null; +} + export interface UpdateProfileRequest { full_name?: string | null; username?: string | null; diff --git a/src/lib/api/api.service.ts b/src/lib/api/api.service.ts index cc5e9687..72a70388 100644 --- a/src/lib/api/api.service.ts +++ b/src/lib/api/api.service.ts @@ -31,6 +31,9 @@ import type { UpdatePreferencesRequest, PaginatedResponse, PaginationQuery, + PushRecipientCandidateDto, + PushTokenDto, + RegisterPushTokenRequest, ReportTemplateDto, ReportTemplateListQuery, ReportTemplateSaveRequest, @@ -186,6 +189,8 @@ const LINE_LINK_START_ENDPOINT = '/line/link-start'; const LINE_LINK_CODE_ENDPOINT = '/line/link-code'; const LINE_LINK_ENDPOINT = '/line/link'; const LINE_RECIPIENTS_ENDPOINT = '/line/recipients'; +const PUSH_TOKENS_ENDPOINT = '/push/tokens'; +const PUSH_RECIPIENTS_ENDPOINT = '/push/recipients'; const DEVICE_LIST_PAGE_SIZE = 1000; function toIsoIfDate(value: string | Date | undefined): string | undefined { @@ -568,6 +573,33 @@ export class ApiService { return this.request(LINE_LINK_ENDPOINT, { method: 'DELETE' }); } + public registerPushToken(payload: RegisterPushTokenRequest): Promise<{ registered: boolean }> { + return this.request<{ registered: boolean }>(PUSH_TOKENS_ENDPOINT, { + method: 'POST', + body: payload + }); + } + + public unregisterPushToken(token: string): Promise { + return this.request(PUSH_TOKENS_ENDPOINT, { method: 'DELETE', query: { token } }); + } + + public getPushTokens(): Promise { + return this.request(PUSH_TOKENS_ENDPOINT, { method: 'GET' }); + } + + public getPushRecipientCandidates( + devEuis: string[], + options: ApiMethodOptions = {} + ): Promise { + // buildQueryString has no array support — join explicitly. + return this.request(PUSH_RECIPIENTS_ENDPOINT, { + method: 'GET', + query: { devEuis: devEuis.join(',') }, + signal: options.signal + }); + } + public getPreferences(): Promise { return this.request(AUTH_PREFERENCES_ENDPOINT, { method: 'GET' }); } diff --git a/src/lib/push/push-notifications.ts b/src/lib/push/push-notifications.ts new file mode 100644 index 00000000..222f81de --- /dev/null +++ b/src/lib/push/push-notifications.ts @@ -0,0 +1,249 @@ +// Web-push (FCM) enrollment for the current browser/device. +// +// The flow is deliberately all client-side: permission and the FCM token are +// properties of THIS browser install, not of the account, so unlike the LINE +// link there is no server action involved — the token is fetched here and +// registered with the API directly. Firebase is loaded lazily inside the +// functions (never at module top level) so the SDK stays out of the SSR path +// and out of every page's initial bundle. +import { browser } from '$app/environment'; +import { + PUBLIC_FIREBASE_API_KEY, + PUBLIC_FIREBASE_APP_ID, + PUBLIC_FIREBASE_AUTH_DOMAIN, + PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + PUBLIC_FIREBASE_PROJECT_ID, + PUBLIC_FIREBASE_VAPID_KEY +} from '$env/static/public'; +import { ApiService } from '$lib/api/api.service'; + +const PUSH_SW_URL = '/push-sw.js'; +const PUSH_SW_SCOPE = '/push-sw/'; +const STORAGE_KEY = 'cw:push-token'; + +export type PushStatus = 'unsupported' | 'ios-needs-install' | 'denied' | 'enabled' | 'disabled'; + +export interface ForegroundPushMessage { + title: string; + body: string; + url: string | null; +} + +function isConfigured(): boolean { + return Boolean( + PUBLIC_FIREBASE_API_KEY && + PUBLIC_FIREBASE_API_KEY !== 'replace-me' && + PUBLIC_FIREBASE_PROJECT_ID && + PUBLIC_FIREBASE_VAPID_KEY + ); +} + +export function isPushSupported(): boolean { + return ( + browser && + isConfigured() && + 'serviceWorker' in navigator && + 'PushManager' in window && + 'Notification' in window + ); +} + +// iOS Safari only exposes the push APIs to PWAs installed on the home screen. +// When the APIs are missing on an iOS device the fix is "install the app", not +// "unsupported browser" — the settings card words the two states differently. +export function iosNeedsInstall(): boolean { + if (!browser) return false; + const isIos = + /iPad|iPhone|iPod/.test(navigator.userAgent) || + // iPadOS 13+ reports as Macintosh but has touch support. + (navigator.userAgent.includes('Macintosh') && navigator.maxTouchPoints > 1); + const isStandalone = window.matchMedia('(display-mode: standalone)').matches; + return isIos && !isStandalone && !isPushSupported(); +} + +export function getPushStatus(): PushStatus { + if (!browser) return 'unsupported'; + if (iosNeedsInstall()) return 'ios-needs-install'; + if (!isPushSupported()) return 'unsupported'; + if (Notification.permission === 'denied') return 'denied'; + if (Notification.permission === 'granted' && getStoredToken()) return 'enabled'; + return 'disabled'; +} + +export function getStoredToken(): string | null { + if (!browser) return null; + try { + return localStorage.getItem(STORAGE_KEY); + } catch { + return null; + } +} + +function setStoredToken(token: string | null): void { + try { + if (token) localStorage.setItem(STORAGE_KEY, token); + else localStorage.removeItem(STORAGE_KEY); + } catch { + // Private browsing modes can refuse storage — enrollment still works, + // the device just re-registers (idempotent upsert) on next load. + } +} + +async function getMessaging() { + const [{ initializeApp, getApps, getApp }, messagingModule] = await Promise.all([ + import('firebase/app'), + import('firebase/messaging') + ]); + + const app = getApps().length + ? getApp() + : initializeApp({ + apiKey: PUBLIC_FIREBASE_API_KEY, + authDomain: PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: PUBLIC_FIREBASE_PROJECT_ID, + messagingSenderId: PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: PUBLIC_FIREBASE_APP_ID + }); + + return { messaging: messagingModule.getMessaging(app), messagingModule }; +} + +async function registerPushServiceWorker(): Promise { + const registration = await navigator.serviceWorker.register(PUSH_SW_URL, { + scope: PUSH_SW_SCOPE + }); + await navigator.serviceWorker.ready.catch(() => undefined); + return registration; +} + +function describeDevice(): string { + const ua = navigator.userAgent; + const platform = /Android/.test(ua) + ? 'Android' + : /iPad|iPhone|iPod/.test(ua) || (ua.includes('Macintosh') && navigator.maxTouchPoints > 1) + ? 'iOS' + : /Windows/.test(ua) + ? 'Windows' + : /Macintosh/.test(ua) + ? 'macOS' + : /Linux/.test(ua) + ? 'Linux' + : 'Unknown'; + const browserName = /Edg\//.test(ua) + ? 'Edge' + : /Chrome\//.test(ua) + ? 'Chrome' + : /Safari\//.test(ua) + ? 'Safari' + : /Firefox\//.test(ua) + ? 'Firefox' + : 'Browser'; + return `${browserName} on ${platform}`; +} + +/** + * Full enrollment for this device. Must be called from a user gesture (click) + * so `Notification.requestPermission()` is allowed to show the prompt (iOS + * requires it, Chrome strongly prefers it). + * Returns the resulting status. + */ +export async function enablePush(authToken: string): Promise { + if (!isPushSupported()) return getPushStatus(); + + const registration = await registerPushServiceWorker(); + + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + return permission === 'denied' ? 'denied' : 'disabled'; + } + + const { messaging, messagingModule } = await getMessaging(); + const token = await messagingModule.getToken(messaging, { + vapidKey: PUBLIC_FIREBASE_VAPID_KEY, + serviceWorkerRegistration: registration + }); + + await new ApiService({ authToken }).registerPushToken({ + token, + deviceLabel: describeDevice() + }); + setStoredToken(token); + return 'enabled'; +} + +/** Remove this device's enrollment (FCM token + API registration). */ +export async function disablePush(authToken: string): Promise { + const stored = getStoredToken(); + + try { + const { messaging, messagingModule } = await getMessaging(); + await messagingModule.deleteToken(messaging); + } catch { + // Token deletion is best-effort; the API row removal below is what stops + // sends, and the engine prunes dead FCM tokens on first failed push. + } + + if (stored) { + await new ApiService({ authToken }).unregisterPushToken(stored).catch(() => undefined); + } + setStoredToken(null); +} + +/** + * Silent refresh on app load for already-enrolled devices: FCM tokens rotate, + * and re-registering the current token bumps `last_seen_at` server-side. No-op + * unless permission is granted and this device previously enrolled. + */ +export async function refreshPushToken(authToken: string): Promise { + if (!isPushSupported()) return; + if (Notification.permission !== 'granted') return; + + const stored = getStoredToken(); + if (!stored) return; + + try { + const registration = await registerPushServiceWorker(); + const { messaging, messagingModule } = await getMessaging(); + const token = await messagingModule.getToken(messaging, { + vapidKey: PUBLIC_FIREBASE_VAPID_KEY, + serviceWorkerRegistration: registration + }); + if (!token) return; + + await new ApiService({ authToken }).registerPushToken({ + token, + deviceLabel: describeDevice() + }); + if (token !== stored) setStoredToken(token); + } catch (error) { + console.warn('Push token refresh failed:', error); + } +} + +/** + * Show pushes that arrive while the app is in the foreground (the service + * worker only receives them when no page is focused). Returns an unsubscribe + * function. No-op unless this device is enrolled. + */ +export async function initForegroundMessages( + onPush: (message: ForegroundPushMessage) => void +): Promise<() => void> { + if (!isPushSupported() || Notification.permission !== 'granted' || !getStoredToken()) { + return () => undefined; + } + + try { + const { messaging, messagingModule } = await getMessaging(); + return messagingModule.onMessage(messaging, (payload) => { + const data = payload.data ?? {}; + onPush({ + title: data.title ?? payload.notification?.title ?? 'CropWatch', + body: data.body ?? payload.notification?.body ?? '', + url: data.url ?? null + }); + }); + } catch (error) { + console.warn('Foreground push listener failed to start:', error); + return () => undefined; + } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index a38fc089..e7ebf3b6 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -18,6 +18,7 @@ import Analytics from '$lib/components/Analytics.svelte'; import WhatsNewDialog from '$lib/components/whats-new/WhatsNewDialog.svelte'; import { createAppContext, defaultAppContext, setAppContext } from '$lib/appContext.svelte'; + import { initForegroundMessages, refreshPushToken } from '$lib/push/push-notifications'; import { createSessionExpiryWatcher } from '$lib/utils/session-expiry'; import { buildLoginPath } from '$lib/utils/auth-redirect'; import type { DeviceStatusSummary, PreferencesDto, RuleTemplateDto } from '$lib/api/api.dtos'; @@ -28,7 +29,7 @@ let { children }: LayoutProps = $props(); - createCwToastContext(); + const toast = createCwToastContext(); let mode = $state('open'); let isAuthRoute = $derived(page.url.pathname.startsWith('/auth')); @@ -97,6 +98,33 @@ sessionWatcher.rearm(); }); + // Push notifications: once per app load, after the first authenticated + // token appears. refreshPushToken silently re-registers an already-enrolled + // device (FCM tokens rotate; also bumps last_seen_at), and the foreground + // listener surfaces pushes as toasts while the app has focus (the service + // worker only shows system notifications when no page is focused). Both + // no-op on devices that never enrolled. Effects never run during SSR. + let pushInitialized = false; + let unsubscribeForegroundPush: (() => void) | null = null; + + $effect(() => { + const accessToken = app.accessToken; + if (!accessToken || pushInitialized) return; + pushInitialized = true; + + void refreshPushToken(accessToken); + void initForegroundMessages(({ title, body }) => { + toast.add({ + tone: 'warning', + message: body ? `${title} — ${body}` : title, + duration: 8000, + dismissible: true + }); + }).then((unsubscribe) => { + unsubscribeForegroundPush = unsubscribe; + }); + }); + function handleWindowResize() { document.querySelector('.cw-sidenav')?.classList.add('cw-sidenav--resizing'); @@ -114,6 +142,7 @@ clearTimeout(resizeTimer); } sessionWatcher.destroy(); + unsubscribeForegroundPush?.(); }); function localizedHref(locale: (typeof locales)[number]): string { diff --git a/src/routes/rules/RuleTemplateForm.svelte b/src/routes/rules/RuleTemplateForm.svelte index 8f987617..14f39c02 100644 --- a/src/routes/rules/RuleTemplateForm.svelte +++ b/src/routes/rules/RuleTemplateForm.svelte @@ -45,6 +45,8 @@ context: RuleFormContextDto; authToken?: string | null; preselectedDevEui?: string | null; + // Used to preselect the rule creator on the default Push action. + currentUserId?: string | null; } interface DeviceSelection { @@ -59,12 +61,19 @@ type EditableTemplateAction = Omit & { config: RuleTemplateActionConfig; - // LINE actions only: the CwMultiSelect selection ({id,label} shape). - // Serialized into config.recipient as {"userIds":[...]} on change. + // LINE / Push actions only: the CwMultiSelect selection ({id,label} + // shape). Serialized into config.recipient as {"userIds":[...]} on change. lineRecipients: { id: string; label: string }[]; + pushRecipients: { id: string; label: string }[]; }; - let { mode, context, authToken = null, preselectedDevEui = null }: Props = $props(); + let { + mode, + context, + authToken = null, + preselectedDevEui = null, + currentUserId = null + }: Props = $props(); let devices = $derived(context.devices); let actions = $derived(context.actionTypes); @@ -97,7 +106,7 @@ let templateActions = $state( initial?.actions.length ? initial.actions.map(createEditableTemplateAction) - : [createBlankTemplateAction()] + : [createDefaultTemplateAction()] ); let actionTypeOptions = $derived( @@ -192,6 +201,53 @@ }); }); + // Eligible push recipients — same fetch pattern as the LINE effect above, + // with its own key/seq guard so the two channels never race each other. + let pushRecipientOptionsBase = $state<{ label: string; value: string }[]>([]); + const pushRecipientNames = new SvelteMap(); + let pushRecipientFetchKey = ''; + let pushRecipientFetchSeq = 0; + + $effect(() => { + const devEuis = [...selectedDevEuis].sort(); + const key = devEuis.join(','); + if (key === pushRecipientFetchKey) return; + pushRecipientFetchKey = key; + + if (devEuis.length === 0 || !authToken) { + pushRecipientOptionsBase = []; + return; + } + + const seq = ++pushRecipientFetchSeq; + new ApiService({ authToken }) + .getPushRecipientCandidates(devEuis) + .then((candidates) => { + if (seq !== pushRecipientFetchSeq) return; + pushRecipientNames.clear(); + pushRecipientOptionsBase = candidates.map((candidate) => { + pushRecipientNames.set(candidate.userId, candidate.displayName); + return { + value: candidate.userId, + label: candidate.pushEnabled + ? candidate.displayName + : `${candidate.displayName}(${m.push_recipient_not_enabled()})` + }; + }); + // Upgrade bare id labels on saved selections now that names are known. + for (const action of templateActions) { + if (action.actionTypeName !== 'Push') continue; + for (const selection of action.pushRecipients) { + const name = pushRecipientNames.get(selection.id); + if (name && selection.label === selection.id) selection.label = name; + } + } + }) + .catch(() => { + if (seq === pushRecipientFetchSeq) pushRecipientOptionsBase = []; + }); + }); + // Saved selections must stay visible even when the fetch hasn't landed or // the user lost device access (same guard pattern as deviceOptions above). function lineRecipientOptionsFor(action: EditableTemplateAction) { @@ -210,7 +266,23 @@ selection.length > 0 ? JSON.stringify({ userIds: selection.map((item) => item.id) }) : ''; } - function parseLineRecipientIds(recipient: string): string[] { + function pushRecipientOptionsFor(action: EditableTemplateAction) { + const missing = action.pushRecipients + .filter((selection) => !pushRecipientOptionsBase.some((o) => o.value === selection.id)) + .map((selection) => ({ value: selection.id, label: selection.label || selection.id })); + return [...missing, ...pushRecipientOptionsBase]; + } + + function setPushRecipients( + action: EditableTemplateAction, + selection: { id: string; label: string }[] + ) { + action.pushRecipients = selection; + action.config.recipient = + selection.length > 0 ? JSON.stringify({ userIds: selection.map((item) => item.id) }) : ''; + } + + function parseRecipientUserIds(recipient: string): string[] { try { const parsed: unknown = JSON.parse(recipient); if ( @@ -239,7 +311,8 @@ (action) => isValidActionTypeId(action.actionType) && action.config.recipient.trim().length > 0 && - (action.actionTypeName !== 'LINE' || action.lineRecipients.length > 0) + (action.actionTypeName !== 'LINE' || action.lineRecipients.length > 0) && + (action.actionTypeName !== 'Push' || action.pushRecipients.length > 0) ) ); let assignmentSummary = $derived( @@ -271,7 +344,12 @@ .map((item) => lineRecipientNames.get(item.id) ?? item.label ?? item.id) .join(', ') || m.line_recipient_preview_count({ count: String(action.lineRecipients.length) }) - : action.config.recipient; + : action.actionTypeName === 'Push' + ? action.pushRecipients + .map((item) => pushRecipientNames.get(item.id) ?? item.label ?? item.id) + .join(', ') || + m.push_recipient_preview_count({ count: String(action.pushRecipients.length) }) + : action.config.recipient; return `${label}: ${recipient}`; }) ); @@ -290,15 +368,45 @@ recipient: '' }, lineRecipients: [], + pushRecipients: [], createdAt: null }; } + // New rules default to a Push action with the creator preselected. The + // bare-id label upgrades once the recipient fetch lands. Falls back to a + // blank action while the Push action type doesn't exist yet server-side. + function createDefaultTemplateAction(): EditableTemplateAction { + if (mode === 'create' && currentUserId) { + const pushType = actions.find((entry) => entry.name === 'Push'); + if (pushType) { + return { + id: 0, + templateId: initial?.id ?? 0, + actionType: pushType.id, + actionTypeName: pushType.name, + actionTypeValue: pushType.value ?? null, + config: { + recipient: JSON.stringify({ userIds: [currentUserId] }) + }, + lineRecipients: [], + pushRecipients: [{ id: currentUserId, label: currentUserId }], + createdAt: null + }; + } + } + return createBlankTemplateAction(); + } + function createEditableTemplateAction(action: RuleTemplateActionDto): EditableTemplateAction { const recipient = readActionRecipient(action); const lineRecipients = action.actionTypeName === 'LINE' - ? parseLineRecipientIds(recipient).map((id) => ({ id, label: id })) + ? parseRecipientUserIds(recipient).map((id) => ({ id, label: id })) + : []; + const pushRecipients = + action.actionTypeName === 'Push' + ? parseRecipientUserIds(recipient).map((id) => ({ id, label: id })) : []; return { ...action, @@ -306,7 +414,8 @@ ...readActionConfig(action.config), recipient }, - lineRecipients + lineRecipients, + pushRecipients }; } @@ -456,16 +565,20 @@ const selectedAction = actions.find((entry) => String(entry.id) === value); const parsedActionType = Number(value); const wasLine = action.actionTypeName === 'LINE'; + const wasPush = action.actionTypeName === 'Push'; action.actionType = selectedAction?.id ?? (Number.isInteger(parsedActionType) ? parsedActionType : 0); action.actionTypeName = selectedAction?.name ?? null; action.actionTypeValue = selectedAction?.value ?? null; - // Switching to or away from LINE always requires a fresh recipient choice. + // Switching to or away from LINE/Push always requires a fresh recipient choice. if (action.actionTypeName === 'LINE' || wasLine) { setLineRecipients(action, []); } + if (action.actionTypeName === 'Push' || wasPush) { + setPushRecipients(action, []); + } } function isValidActionTypeId(value: number): boolean { @@ -633,6 +746,17 @@ searchable required /> + {:else if action.actionTypeName === 'Push'} + setPushRecipients(action, selection)} + searchable + required + /> {/if} diff --git a/src/routes/rules/create/+page.svelte b/src/routes/rules/create/+page.svelte index a0a391dc..8344ecde 100644 --- a/src/routes/rules/create/+page.svelte +++ b/src/routes/rules/create/+page.svelte @@ -33,6 +33,7 @@ context={data.context} authToken={data.authToken} preselectedDevEui={data.devEui} + currentUserId={data.session?.sub ?? null} /> diff --git a/src/routes/rules/edit/[id]/+page.svelte b/src/routes/rules/edit/[id]/+page.svelte index 4cffc688..b42d2575 100644 --- a/src/routes/rules/edit/[id]/+page.svelte +++ b/src/routes/rules/edit/[id]/+page.svelte @@ -35,7 +35,12 @@ /> - +