Official URL migration preserves sessions, shortcuts prefs, and last clock-in client; auth screens match web with legal links; time clock and invoice editor/send flows are updated for the new domain and UI patterns. Co-authored-by: Cursor <cursoragent@cursor.com>
133 lines
3.7 KiB
TypeScript
133 lines
3.7 KiB
TypeScript
import { HStack, Image, Spacer, Text } from "@expo/ui/swift-ui";
|
|
import {
|
|
font,
|
|
foregroundStyle,
|
|
lineLimit,
|
|
minimumScaleFactor,
|
|
monospacedDigit,
|
|
padding,
|
|
widgetAccentedRenderingMode,
|
|
} from "@expo/ui/swift-ui/modifiers";
|
|
import { createLiveActivity, type LiveActivityEnvironment } from "expo-widgets";
|
|
|
|
import type { TimeClockActivityProps } from "@/lib/time-clock-live-activity.types";
|
|
|
|
const TIMER_HORIZON_MS = 24 * 60 * 60 * 1000;
|
|
|
|
function liveTimer(
|
|
startedAtMs: number,
|
|
modifiers: ReturnType<typeof font>[],
|
|
) {
|
|
const lower = new Date(startedAtMs);
|
|
const upper = new Date(startedAtMs + TIMER_HORIZON_MS);
|
|
|
|
return (
|
|
<Text
|
|
timerInterval={{ lower, upper }}
|
|
countsDown={false}
|
|
modifiers={modifiers}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function TimeClockActivity(props: TimeClockActivityProps, _environment: LiveActivityEnvironment) {
|
|
"widget";
|
|
|
|
const green = "green";
|
|
const title = props.description.trim() || "Clock In";
|
|
const clientLabel = props.clientName.trim() || title;
|
|
const subtitle = props.invoiceLabel.trim();
|
|
const startedAtMs = props.startedAtMs > 0 ? props.startedAtMs : Date.now();
|
|
|
|
const timerMods = [
|
|
font({ design: "monospaced", weight: "bold", size: 20 }),
|
|
monospacedDigit(),
|
|
foregroundStyle(green),
|
|
lineLimit(1),
|
|
minimumScaleFactor(0.85),
|
|
];
|
|
const compactTimerMods = [
|
|
font({ design: "monospaced", weight: "semibold", size: 11 }),
|
|
monospacedDigit(),
|
|
foregroundStyle(green),
|
|
lineLimit(1),
|
|
minimumScaleFactor(0.8),
|
|
];
|
|
const clientMods = [
|
|
font({ weight: "semibold", size: 13 }),
|
|
foregroundStyle({ type: "hierarchical", style: "primary" }),
|
|
lineLimit(1),
|
|
minimumScaleFactor(0.85),
|
|
];
|
|
const subtitleMods = [
|
|
font({ size: 11 }),
|
|
foregroundStyle({ type: "hierarchical", style: "secondary" }),
|
|
lineLimit(1),
|
|
minimumScaleFactor(0.85),
|
|
];
|
|
|
|
const bannerTimer = liveTimer(startedAtMs, timerMods);
|
|
const compactTimer = liveTimer(startedAtMs, compactTimerMods);
|
|
|
|
return {
|
|
banner: (
|
|
<HStack alignment="center" spacing={8} modifiers={[padding({ horizontal: 14, vertical: 12 })]}>
|
|
<Image
|
|
systemName="dollarsign.circle.fill"
|
|
color={green}
|
|
size={22}
|
|
modifiers={[widgetAccentedRenderingMode("fullColor")]}
|
|
/>
|
|
<Text modifiers={clientMods}>{clientLabel}</Text>
|
|
<Spacer minLength={12} />
|
|
{bannerTimer}
|
|
</HStack>
|
|
),
|
|
bannerSmall: (
|
|
<HStack alignment="center" spacing={8} modifiers={[padding({ horizontal: 12, vertical: 10 })]}>
|
|
<Image
|
|
systemName="dollarsign.circle.fill"
|
|
color={green}
|
|
size={18}
|
|
modifiers={[widgetAccentedRenderingMode("fullColor")]}
|
|
/>
|
|
<Text modifiers={clientMods}>{clientLabel}</Text>
|
|
<Spacer minLength={8} />
|
|
{compactTimer}
|
|
</HStack>
|
|
),
|
|
compactLeading: (
|
|
<Image
|
|
systemName="dollarsign.circle.fill"
|
|
color={green}
|
|
size={15}
|
|
modifiers={[widgetAccentedRenderingMode("fullColor")]}
|
|
/>
|
|
),
|
|
compactTrailing: compactTimer,
|
|
minimal: (
|
|
<Image
|
|
systemName="dollarsign.circle.fill"
|
|
color={green}
|
|
size={12}
|
|
modifiers={[widgetAccentedRenderingMode("fullColor")]}
|
|
/>
|
|
),
|
|
expandedLeading: (
|
|
<Image
|
|
systemName="dollarsign.circle.fill"
|
|
color={green}
|
|
size={20}
|
|
modifiers={[widgetAccentedRenderingMode("fullColor")]}
|
|
/>
|
|
),
|
|
expandedCenter: <Text modifiers={clientMods}>{clientLabel}</Text>,
|
|
expandedTrailing: bannerTimer,
|
|
expandedBottom: (
|
|
<Text modifiers={subtitleMods}>{subtitle || "beenvoice"}</Text>
|
|
),
|
|
};
|
|
}
|
|
|
|
export default createLiveActivity("TimeClockActivity", TimeClockActivity);
|