Skip to content

Commit 5be0b4c

Browse files
committed
Setting calendar events goes through settings
1 parent 86e3266 commit 5be0b4c

3 files changed

Lines changed: 66 additions & 6 deletions

File tree

mobile/modules/engine/src/stores/settings.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,12 @@ const getDefaultSettings = () =>
788788
let loadAllSettingsInFlight: AsyncResult<void, Error> | null = null
789789

790790
function printableSettingValue(key: string, value: unknown): string {
791-
return key.includes("token") || key.includes("email") ? "<redacted>" : JSON.stringify(value)
791+
if (key.includes("token") || key.includes("email")) return "<redacted>"
792+
// Calendar events carry event titles and locations. log the shape, not the contents
793+
if (key === SETTINGS.calendar_events.key) {
794+
return Array.isArray(value) ? `<${value.length} event(s)>` : "<redacted>"
795+
}
796+
return JSON.stringify(value)
792797
}
793798

794799
export const useSettingsStore = create<SettingsState>()(

mobile/src/services/MantleManager.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {waitFor} from "@testing-library/react-native"
2+
import * as Calendar from "expo-calendar"
23
import {router} from "expo-router"
34

45
import mantle from "@/services/MantleManager"
@@ -67,6 +68,9 @@ jest.mock("@/utils/e2eMetrics", () => ({
6768
}))
6869

6970
jest.mock("expo-calendar", () => ({
71+
// Denied by default: the boot-time sync then skips quietly, and only the
72+
// calendar test below opts into the granted path.
73+
getCalendarPermissionsAsync: jest.fn(() => Promise.resolve({status: "denied"})),
7074
getCalendarsAsync: jest.fn(() => Promise.resolve([])),
7175
getEventsAsync: jest.fn(() => Promise.resolve([])),
7276
EntityTypes: {EVENT: "event"},
@@ -287,6 +291,52 @@ describe("MantleManager", () => {
287291
)
288292
})
289293

294+
it("writes calendar events through the settings store so full pushes carry them", async () => {
295+
useGlassesStore.getState().setGlassesInfo({deviceModel: "Even Realities G1"})
296+
;(Calendar.getCalendarPermissionsAsync as jest.Mock).mockResolvedValueOnce({status: "granted"})
297+
;(Calendar.getCalendarsAsync as jest.Mock).mockResolvedValueOnce([{id: "cal-1"}])
298+
;(Calendar.getEventsAsync as jest.Mock).mockResolvedValueOnce([
299+
{title: "Design review", startDate: "2026-08-03T12:00:00.000Z", endDate: "2026-08-03T13:00:00.000Z"},
300+
{
301+
title: "Standup",
302+
location: "Room 4",
303+
startDate: "2026-08-02T15:00:00.000Z",
304+
endDate: "2026-08-02T16:00:00.000Z",
305+
},
306+
{title: "Retro", startDate: "2026-08-04T12:00:00.000Z", endDate: "2026-08-04T12:30:00.000Z"},
307+
{title: "Breakfast", startDate: "2026-08-02T09:00:00.000Z", endDate: "2026-08-02T09:30:00.000Z"},
308+
])
309+
;(bluetoothSdkMock.updateBluetoothSettings as jest.Mock).mockClear()
310+
311+
await (mantle as unknown as {sendCalendarEvents: () => Promise<void>}).sendCalendarEvents()
312+
313+
const expected = [
314+
{title: "Breakfast", time: expect.any(String), endDate: Date.parse("2026-08-02T09:30:00.000Z") / 1000},
315+
{
316+
title: "Standup",
317+
location: "Room 4",
318+
time: expect.any(String),
319+
endDate: Date.parse("2026-08-02T16:00:00.000Z") / 1000,
320+
},
321+
{title: "Design review", time: expect.any(String), endDate: Date.parse("2026-08-03T13:00:00.000Z") / 1000},
322+
]
323+
expect(useSettingsStore.getState().getSetting(SETTINGS.calendar_events.key)).toEqual(expected)
324+
expect(useSettingsStore.getState().getBluetoothSettings().calendar_events).toEqual(expected)
325+
expect(bluetoothSdkMock.setCalendarEvents).not.toHaveBeenCalled()
326+
327+
jest.runOnlyPendingTimers()
328+
expect(bluetoothSdkMock.updateBluetoothSettings).toHaveBeenCalledWith(
329+
expect.objectContaining({calendar_events: expected}),
330+
)
331+
;(bluetoothSdkMock.updateBluetoothSettings as jest.Mock).mockClear()
332+
emitBluetoothSdkEvent("glasses_status", {connection: {state: "connected", fullyBooted: true}})
333+
await waitFor(() => {
334+
expect(bluetoothSdkMock.updateBluetoothSettings).toHaveBeenCalledWith(
335+
expect.objectContaining({calendar_events: expected}),
336+
)
337+
})
338+
})
339+
290340
it("syncs standalone WiFi status events into the glasses store", () => {
291341
emitBluetoothSdkEvent("wifi_status_change", {
292342
type: "wifi_status_change",

mobile/src/services/MantleManager.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import BluetoothSdk from "@mentra/bluetooth-sdk-internal"
1+
import BluetoothSdk, {type CalendarEvent} from "@mentra/bluetooth-sdk-internal"
22
import CrustModule from "@mentra/crust"
33
import {Asset} from "expo-asset"
44
import * as Calendar from "expo-calendar"
@@ -1025,10 +1025,15 @@ class MantleManager {
10251025
endDate: Math.floor(end.getTime() / 1000),
10261026
}
10271027
})
1028-
try {
1029-
await BluetoothSdk.setCalendarEvents(shapedEvents)
1030-
} catch (error) {
1031-
console.warn("MANTLE: Failed to sync calendar events to glasses", error)
1028+
console.log(`MANTLE: calendar sync: calendars=${calendars.length} pushing=${shapedEvents.length} event(s)`)
1029+
// Write through the settings store, not BluetoothSdk.setCalendarEvents().
1030+
// `calendar_events` is a BLUETOOTH_SETTING_KEY, so every full push
1031+
// (pushAllBluetoothSettings before connect, pushDeviceSettingsOnConnect on
1032+
// the connected transition) overwrites the native DeviceStore copy with
1033+
// whatever the store holds.
1034+
const res = await engine.settings.set<CalendarEvent[]>(SETTINGS.calendar_events.key, shapedEvents)
1035+
if (res.is_error()) {
1036+
console.warn("MANTLE: Failed to sync calendar events to glasses", res.error)
10321037
}
10331038
} catch (error) {
10341039
// it's fine if this fails

0 commit comments

Comments
 (0)