Redesign mobile time clock, add shortcuts, and improve account management.

Add iOS Shortcuts/Siri intents, local send-reminder notifications, stable
client picker with last-client defaults, account refresh/remove, and softer
session handling on unauthorized API responses.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-22 16:06:17 -04:00
co-authored by Cursor
parent 0b2d65a4e9
commit 06bc91ac13
33 changed files with 1844 additions and 320 deletions
@@ -0,0 +1,36 @@
import AppIntents
@available(iOS 16.0, *)
struct BeenVoiceShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
[
AppShortcut(
intent: ClockInIntent(),
phrases: [
"Clock in with \(.applicationName)",
"Start timer in \(.applicationName)",
],
shortTitle: "Clock In",
systemImageName: "play.circle.fill"
),
AppShortcut(
intent: ClockOutIntent(),
phrases: [
"Clock out in \(.applicationName)",
"Stop timer in \(.applicationName)",
],
shortTitle: "Clock Out",
systemImageName: "stop.circle.fill"
),
AppShortcut(
intent: OpenTimerIntent(),
phrases: [
"Open time clock in \(.applicationName)",
"Open timer in \(.applicationName)",
],
shortTitle: "Time Clock",
systemImageName: "timer"
),
]
}
}
+33
View File
@@ -0,0 +1,33 @@
import AppIntents
import UIKit
@available(iOS 16.0, *)
struct ClockInIntent: AppIntent {
static var title: LocalizedStringResource = "Clock In"
static var description = IntentDescription("Start the beenvoice time clock with your last client.")
static var openAppWhenRun: Bool = false
@Parameter(title: "Title")
var title: String?
func perform() async throws -> some IntentResult {
var components = URLComponents()
components.scheme = "beenvoice"
components.host = "shortcuts"
components.path = "/clock-in"
if let title, !title.isEmpty {
components.queryItems = [URLQueryItem(name: "title", value: title)]
}
guard let url = components.url else {
return .result()
}
await MainActor.run {
UIApplication.shared.open(url)
}
return .result()
}
}
+21
View File
@@ -0,0 +1,21 @@
import AppIntents
import UIKit
@available(iOS 16.0, *)
struct ClockOutIntent: AppIntent {
static var title: LocalizedStringResource = "Clock Out"
static var description = IntentDescription("Stop the running beenvoice timer and save your time.")
static var openAppWhenRun: Bool = false
func perform() async throws -> some IntentResult {
guard let url = URL(string: "beenvoice://shortcuts/clock-out") else {
return .result()
}
await MainActor.run {
UIApplication.shared.open(url)
}
return .result()
}
}
+21
View File
@@ -0,0 +1,21 @@
import AppIntents
import UIKit
@available(iOS 16.0, *)
struct OpenTimerIntent: AppIntent {
static var title: LocalizedStringResource = "Open Time Clock"
static var description = IntentDescription("Open the beenvoice time clock.")
static var openAppWhenRun: Bool = false
func perform() async throws -> some IntentResult {
guard let url = URL(string: "beenvoice://timer") else {
return .result()
}
await MainActor.run {
UIApplication.shared.open(url)
}
return .result()
}
}
+76
View File
@@ -0,0 +1,76 @@
// @ts-check
const {
withDangerousMod,
withXcodeProject,
IOSConfig,
} = require("@expo/config-plugins");
const fs = require("fs");
const path = require("path");
const SWIFT_FILES = [
"ClockInIntent.swift",
"ClockOutIntent.swift",
"OpenTimerIntent.swift",
"BeenVoiceShortcuts.swift",
];
/** @type {import('@expo/config-plugins').ConfigPlugin} */
function withAppIntents(config) {
const appIntentsSource = path.join(
config._internal?.projectRoot ?? process.cwd(),
"plugins",
"app-intents",
);
config = withDangerousMod(config, [
"ios",
async (config) => {
const platformRoot = config.modRequest.platformProjectRoot;
const projectName = IOSConfig.XcodeUtils.getProjectName(platformRoot);
const targetDir = path.join(platformRoot, projectName);
fs.mkdirSync(targetDir, { recursive: true });
for (const file of SWIFT_FILES) {
fs.copyFileSync(path.join(appIntentsSource, file), path.join(targetDir, file));
}
return config;
},
]);
return withXcodeProject(config, (config) => {
const project = config.modResults;
const platformRoot = config.modRequest.platformProjectRoot;
const projectName = IOSConfig.XcodeUtils.getProjectName(platformRoot);
for (const file of SWIFT_FILES) {
const filepath = `${projectName}/${file}`;
const absolutePath = path.join(platformRoot, filepath);
if (!fs.existsSync(absolutePath)) continue;
const fileRef = project.pbxFileReferenceSection();
const alreadyLinked = Object.values(fileRef).some(
(entry) =>
entry &&
typeof entry === "object" &&
"path" in entry &&
entry.path === file,
);
if (!alreadyLinked) {
IOSConfig.XcodeUtils.addBuildSourceFileToGroup({
filepath,
groupName: projectName,
project,
verbose: true,
});
}
}
return config;
});
}
module.exports = withAppIntents;