diff --git a/mobile/package-lock.json b/mobile/package-lock.json index a6fac78..9349a24 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -1,19 +1,23 @@ { - "name": "bytelyst-agentic-notes-mobile", + "name": "@notelett/mobile", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "bytelyst-agentic-notes-mobile", + "name": "@notelett/mobile", "version": "0.1.0", "dependencies": { "@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client", "@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client", + "@bytelyst/blob-client": "file:../../learning_ai_common_plat/packages/blob-client", "@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens", + "@bytelyst/feature-flag-client": "file:../../learning_ai_common_plat/packages/feature-flag-client", + "@bytelyst/kill-switch-client": "file:../../learning_ai_common_plat/packages/kill-switch-client", "@bytelyst/offline-queue": "file:../../learning_ai_common_plat/packages/offline-queue", "@bytelyst/platform-client": "file:../../learning_ai_common_plat/packages/platform-client", "@bytelyst/react-native-platform-sdk": "file:../../learning_ai_common_plat/packages/react-native-platform-sdk", + "@bytelyst/telemetry-client": "file:../../learning_ai_common_plat/packages/telemetry-client", "expo": "~55.0.4", "expo-router": "~6.0.4", "expo-status-bar": "~3.0.9", @@ -45,6 +49,10 @@ "name": "@bytelyst/auth-client", "version": "0.1.0" }, + "../../learning_ai_common_plat/packages/blob-client": { + "name": "@bytelyst/blob-client", + "version": "0.1.0" + }, "../../learning_ai_common_plat/packages/design-tokens": { "name": "@bytelyst/design-tokens", "version": "0.1.0", @@ -52,6 +60,14 @@ "tsx": "^4.0.0" } }, + "../../learning_ai_common_plat/packages/feature-flag-client": { + "name": "@bytelyst/feature-flag-client", + "version": "0.1.0" + }, + "../../learning_ai_common_plat/packages/kill-switch-client": { + "name": "@bytelyst/kill-switch-client", + "version": "0.1.0" + }, "../../learning_ai_common_plat/packages/offline-queue": { "name": "@bytelyst/offline-queue", "version": "0.1.0" @@ -81,6 +97,10 @@ "react-native": ">=0.72.0" } }, + "../../learning_ai_common_plat/packages/telemetry-client": { + "name": "@bytelyst/telemetry-client", + "version": "0.1.0" + }, "../learning_ai_common_plat/packages/api-client": { "extraneous": true }, @@ -1586,10 +1606,22 @@ "resolved": "../../learning_ai_common_plat/packages/auth-client", "link": true }, + "node_modules/@bytelyst/blob-client": { + "resolved": "../../learning_ai_common_plat/packages/blob-client", + "link": true + }, "node_modules/@bytelyst/design-tokens": { "resolved": "../../learning_ai_common_plat/packages/design-tokens", "link": true }, + "node_modules/@bytelyst/feature-flag-client": { + "resolved": "../../learning_ai_common_plat/packages/feature-flag-client", + "link": true + }, + "node_modules/@bytelyst/kill-switch-client": { + "resolved": "../../learning_ai_common_plat/packages/kill-switch-client", + "link": true + }, "node_modules/@bytelyst/offline-queue": { "resolved": "../../learning_ai_common_plat/packages/offline-queue", "link": true @@ -1602,6 +1634,10 @@ "resolved": "../../learning_ai_common_plat/packages/react-native-platform-sdk", "link": true }, + "node_modules/@bytelyst/telemetry-client": { + "resolved": "../../learning_ai_common_plat/packages/telemetry-client", + "link": true + }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://jfrog-pkg-proxy.it.att.com/artifactory/api/npm/att-npm-proxy-group/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", diff --git a/mobile/package.json b/mobile/package.json index 7fa01a3..e72c3c1 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -16,9 +16,13 @@ "dependencies": { "@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client", "@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client", + "@bytelyst/blob-client": "file:../../learning_ai_common_plat/packages/blob-client", "@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens", + "@bytelyst/feature-flag-client": "file:../../learning_ai_common_plat/packages/feature-flag-client", + "@bytelyst/kill-switch-client": "file:../../learning_ai_common_plat/packages/kill-switch-client", "@bytelyst/offline-queue": "file:../../learning_ai_common_plat/packages/offline-queue", "@bytelyst/platform-client": "file:../../learning_ai_common_plat/packages/platform-client", + "@bytelyst/telemetry-client": "file:../../learning_ai_common_plat/packages/telemetry-client", "@bytelyst/react-native-platform-sdk": "file:../../learning_ai_common_plat/packages/react-native-platform-sdk", "expo": "~55.0.4", "expo-router": "~6.0.4", diff --git a/mobile/src/app/_layout.tsx b/mobile/src/app/_layout.tsx index d763f7a..2f648e1 100644 --- a/mobile/src/app/_layout.tsx +++ b/mobile/src/app/_layout.tsx @@ -5,6 +5,7 @@ import { useAuthStore, type AuthState } from '../store/auth-store'; import { useInboxStore, type InboxState } from '../store/inbox-store'; import { useNotesStore, type NotesState } from '../store/notes-store'; import { useWorkspaceStore, type WorkspaceState } from '../store/workspace-store'; +import { initPlatform } from '../lib/platform'; export default function RootLayout() { const bootstrapAuth = useAuthStore((state: AuthState) => state.bootstrap); @@ -14,6 +15,7 @@ export default function RootLayout() { useEffect(() => { void bootstrapAuth(); + void initPlatform(); void hydrateNotes(); void hydrateWorkspaces(); void hydrateInbox(); diff --git a/mobile/src/lib/platform.ts b/mobile/src/lib/platform.ts new file mode 100644 index 0000000..5ea34ee --- /dev/null +++ b/mobile/src/lib/platform.ts @@ -0,0 +1,64 @@ +import { createTelemetryClient } from '@bytelyst/telemetry-client'; +import { createFeatureFlagClient } from '@bytelyst/feature-flag-client'; +import { createKillSwitchClient } from '@bytelyst/kill-switch-client'; +import { createBlobClient } from '@bytelyst/blob-client'; +import { API_CONFIG, PRODUCT_ID } from '../api/config'; +import { mmkvStorage } from '../store/mmkv-storage'; + +function getAccessToken(): string | null { + return mmkvStorage.getItem(`${PRODUCT_ID}_access_token`); +} + +export const telemetryClient = createTelemetryClient({ + productId: PRODUCT_ID, + baseUrl: API_CONFIG.platformBaseUrl, + endpoint: '/telemetry/events', + platform: 'mobile', + channel: 'notelett_mobile', + transport: 'fetch', + appVersion: '0.1.0', + buildNumber: '1', + releaseChannel: 'dev', + osFamily: 'other', +}); + +export const featureFlagClient = createFeatureFlagClient({ + baseUrl: API_CONFIG.platformBaseUrl, + productId: PRODUCT_ID, + platform: 'mobile', + pollIntervalMs: 5 * 60 * 1000, +}); + +export const killSwitchClient = createKillSwitchClient({ + baseUrl: API_CONFIG.platformBaseUrl, + productId: PRODUCT_ID, + platform: 'mobile', +}); + +export const blobClient = createBlobClient({ + baseUrl: API_CONFIG.platformBaseUrl, + productId: PRODUCT_ID, + getAccessToken, +}); + +let platformInitialized = false; + +export async function initPlatform(): Promise { + if (platformInitialized) return; + platformInitialized = true; + + telemetryClient.init(); + telemetryClient.trackEvent('info', 'app_shell', 'mobile_app_initialized'); + + await featureFlagClient.init().catch(() => { + // Feature flags are best-effort + }); +} + +export function isFeatureEnabled(key: string): boolean { + return featureFlagClient.isEnabled(key); +} + +export async function checkKillSwitch(): Promise<{ disabled: boolean; message: string | null }> { + return killSwitchClient.check(); +} diff --git a/web/package-lock.json b/web/package-lock.json index ae9d892..769d55b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client", "@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client", + "@bytelyst/blob-client": "file:../../learning_ai_common_plat/packages/blob-client", "@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens", "@bytelyst/diagnostics-client": "file:../../learning_ai_common_plat/packages/diagnostics-client", "@bytelyst/feature-flag-client": "file:../../learning_ai_common_plat/packages/feature-flag-client", @@ -47,6 +48,10 @@ "name": "@bytelyst/auth-client", "version": "0.1.0" }, + "../../learning_ai_common_plat/packages/blob-client": { + "name": "@bytelyst/blob-client", + "version": "0.1.0" + }, "../../learning_ai_common_plat/packages/design-tokens": { "name": "@bytelyst/design-tokens", "version": "0.1.0", @@ -460,6 +465,10 @@ "resolved": "../../learning_ai_common_plat/packages/auth-client", "link": true }, + "node_modules/@bytelyst/blob-client": { + "resolved": "../../learning_ai_common_plat/packages/blob-client", + "link": true + }, "node_modules/@bytelyst/design-tokens": { "resolved": "../../learning_ai_common_plat/packages/design-tokens", "link": true diff --git a/web/package.json b/web/package.json index 34e7920..abeec95 100644 --- a/web/package.json +++ b/web/package.json @@ -15,6 +15,7 @@ "@bytelyst/api-client": "file:../../learning_ai_common_plat/packages/api-client", "@bytelyst/auth-client": "file:../../learning_ai_common_plat/packages/auth-client", "@bytelyst/design-tokens": "file:../../learning_ai_common_plat/packages/design-tokens", + "@bytelyst/blob-client": "file:../../learning_ai_common_plat/packages/blob-client", "@bytelyst/diagnostics-client": "file:../../learning_ai_common_plat/packages/diagnostics-client", "@bytelyst/feature-flag-client": "file:../../learning_ai_common_plat/packages/feature-flag-client", "@bytelyst/kill-switch-client": "file:../../learning_ai_common_plat/packages/kill-switch-client", diff --git a/web/src/lib/blob-client.ts b/web/src/lib/blob-client.ts index a7f7165..70e393b 100644 --- a/web/src/lib/blob-client.ts +++ b/web/src/lib/blob-client.ts @@ -1,46 +1,36 @@ "use client"; -import { platformClient } from "@/lib/platform"; +import { createBlobClient } from "@bytelyst/blob-client"; +import { PLATFORM_SERVICE_URL, PRODUCT_ID } from "@/lib/product-config"; -type BlobSasResponse = { - sasUrl: string; -}; +function getAccessToken(): string | null { + if (typeof window === "undefined") return null; + return localStorage.getItem(`${PRODUCT_ID}_access_token`); +} + +const blobClient = createBlobClient({ + baseUrl: PLATFORM_SERVICE_URL, + productId: PRODUCT_ID, + getAccessToken, +}); export async function getArtifactReadUrl(blobPath: string): Promise { - const response = await platformClient.post("/blob/sas", { - container: "attachments", - blobName: blobPath, - permissions: "r", - expiresInMinutes: 15, - }); - - return response.sasUrl; + const sas = await blobClient.getSasUrl("attachments", blobPath, "r", 15); + return sas.sasUrl; } export async function getArtifactUploadUrl(blobPath: string): Promise { - const response = await platformClient.post("/blob/sas", { - container: "attachments", - blobName: blobPath, - permissions: "cw", - expiresInMinutes: 30, - }); - - return response.sasUrl; + const sas = await blobClient.getSasUrl("attachments", blobPath, "rw", 30); + return sas.sasUrl; } export async function uploadArtifact( file: File, blobPath: string, ): Promise<{ blobPath: string; contentType: string; sizeBytes: number }> { - const sasUrl = await getArtifactUploadUrl(blobPath); - - await fetch(sasUrl, { - method: "PUT", - headers: { - "x-ms-blob-type": "BlockBlob", - "Content-Type": file.type || "application/octet-stream", - }, - body: file, + await blobClient.upload("attachments", file, { + contentType: file.type || "application/octet-stream", + blobName: blobPath, }); return { @@ -51,7 +41,8 @@ export async function uploadArtifact( } export async function downloadArtifact(blobPath: string): Promise { - const sasUrl = await getArtifactReadUrl(blobPath); - const response = await fetch(sasUrl); + const response = await blobClient.download("attachments", blobPath); return response.blob(); } + +export { blobClient };