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:
@@ -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"
|
||||
),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user