Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions ios/ancla-app/app-view-model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ final class AppViewModel {
var draftSelection = FamilyActivitySelection()
var draftModeName = "Work block"
var draftModeShouldBeDefault = false
var draftModeIsStrict = false
var draftTagName = "Desk anchor"
var selectedModeID: UUID?
var isPickerPresented = false
Expand Down Expand Up @@ -159,6 +160,10 @@ final class AppViewModel {
AnclaCore.canArmSelectedMode(snapshot)
}

var currentModeIsStrict: Bool {
(selectedMode() ?? preferredMode())?.isStrict == true
}

func load() {
do {
snapshot = AnclaCore.repairedSnapshot(try store.load())
Expand Down Expand Up @@ -204,6 +209,7 @@ final class AppViewModel {
clearDefaultFlag()
}
mode.isDefault = shouldBeDefault
mode.isStrict = draftModeIsStrict
snapshot.modes[index] = mode
ensureDefaultMode()

Expand All @@ -218,13 +224,15 @@ final class AppViewModel {
mode = BlockMode(
name: modeName,
selectionData: Data(),
isDefault: snapshot.modes.isEmpty || draftModeShouldBeDefault
isDefault: snapshot.modes.isEmpty || draftModeShouldBeDefault,
isStrict: draftModeIsStrict
)
} else {
mode = try BlockMode(
name: modeName,
selection: draftSelection,
isDefault: snapshot.modes.isEmpty || draftModeShouldBeDefault
isDefault: snapshot.modes.isEmpty || draftModeShouldBeDefault,
isStrict: draftModeIsStrict
)
}

Expand All @@ -251,6 +259,7 @@ final class AppViewModel {
draftModeName = "Work block"
draftSelection = FamilyActivitySelection()
draftModeShouldBeDefault = snapshot.modes.isEmpty
draftModeIsStrict = false
}

func prepareDraftForEditingMode(_ modeID: UUID) {
Expand All @@ -263,6 +272,7 @@ final class AppViewModel {
draftModeID = mode.id
draftModeName = mode.name
draftModeShouldBeDefault = mode.isDefault
draftModeIsStrict = mode.isStrict

if isSideloadLiteBuild && mode.selectionData.isEmpty {
draftSelection = FamilyActivitySelection()
Expand Down
77 changes: 76 additions & 1 deletion ios/ancla-app/content-view.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct ContentView: View {
private let bottomActionBarClearance: CGFloat = 132

@State private var isModeEditorPresented = false
@State private var isShortcutGuidesPresented = false
@State private var renamingAnchorID: UUID?
@State private var anchorNameDraft = ""

Expand Down Expand Up @@ -58,6 +59,10 @@ struct ContentView: View {
renameAnchorSheet
.presentationBackground(.clear)
}
.sheet(isPresented: $isShortcutGuidesPresented) {
ShortcutGuidesSheet()
.presentationBackground(.clear)
}
.anclaFamilyActivityPicker(
isPresented: $viewModel.isPickerPresented,
selection: $viewModel.draftSelection
Expand Down Expand Up @@ -131,6 +136,13 @@ struct ContentView: View {

private var headlineSection: some View {
VStack(alignment: .leading, spacing: 10) {
if viewModel.currentModeIsStrict {
Text("STRICT MODE")
.font(.ancla(11, weight: .semibold))
.foregroundStyle(AnclaTheme.warningText)
.tracking(1.6)
}

Text(viewModel.diagnostics.headline)
.font(.ancla(40, weight: .medium))
.foregroundStyle(AnclaTheme.primaryText)
Expand Down Expand Up @@ -190,6 +202,38 @@ struct ContentView: View {
}
)

if viewModel.currentModeIsStrict {
surfaceDivider

sectionBlock(
title: "Strict mode",
content: {
VStack(spacing: 12) {
informativeRow(
title: strictModeTitle,
detail: strictModeDetail,
accentColor: AnclaTheme.warningText,
highlight: viewModel.canReleaseActiveSession,
trailingText: "Tighter"
)

Button {
isShortcutGuidesPresented = true
} label: {
actionRow(
icon: "bolt.horizontal.circle",
title: "Review Apple app guides",
detail: "Safari, Settings, Messages, Mail, Phone, and Calendar still need Shortcuts automations.",
isLoading: false
)
}
.buttonStyle(AnclaPressableButtonStyle())
.disabled(viewModel.isBusy)
}
}
)
}

surfaceDivider

sectionBlock(
Expand Down Expand Up @@ -555,6 +599,12 @@ struct ContentView: View {

Spacer(minLength: 0)

if mode.isStrict {
Text("Strict")
.font(.ancla(12, weight: .medium))
.foregroundStyle(AnclaTheme.warningText)
}

if isArmed {
Text("Active")
.font(.ancla(12, weight: .medium))
Expand Down Expand Up @@ -798,7 +848,12 @@ struct ContentView: View {
return "Create or choose a mode before starting a session."
}

return viewModel.selectionSummary(for: currentMode)
let summary = viewModel.selectionSummary(for: currentMode)
guard currentMode.isStrict else {
return summary
}

return "\(summary) • strict mode"
}

private var anchorDetail: String {
Expand Down Expand Up @@ -846,6 +901,10 @@ struct ContentView: View {
case .armed:
return sessionWaitingDetail
case .mismatchedTag:
if viewModel.currentModeIsStrict {
return "A different anchor was scanned. Strict mode stays active until the right anchor is used. \(emergencyCountSentence)"
}

return "A different anchor was scanned. The session remains active. \(emergencyCountSentence)"
case .released:
return "The most recent session was released successfully."
Expand Down Expand Up @@ -1073,6 +1132,10 @@ struct ContentView: View {

private var sessionWaitingDetail: String {
if let activePairedTag = viewModel.activePairedTag {
if viewModel.currentModeIsStrict {
return "Strict mode is active. \(activePairedTag.displayName) is the only release path. \(emergencyCountSentence)"
}

return "The current session remains active until \(activePairedTag.displayName) is scanned. \(emergencyCountSentence)"
}

Expand Down Expand Up @@ -1156,6 +1219,18 @@ struct ContentView: View {

return "Remove this paired anchor from this iPhone."
}

private var strictModeTitle: String {
viewModel.canReleaseActiveSession ? "Strict mode is active" : "Strict mode is ready"
}

private var strictModeDetail: String {
if viewModel.canReleaseActiveSession {
return "This session is meant to feel harder to bypass. Close obvious loopholes with the Apple app shortcut guides before you rely on it."
}

return "This mode uses stronger, more committed copy and a native-Apple-app checklist so the easy bypasses are harder to ignore."
}
}

private extension View {
Expand Down
76 changes: 75 additions & 1 deletion ios/ancla-app/mode-editor-view.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ struct ModeEditorView: View {
let onChooseSelection: () -> Void

@Environment(\.dismiss) private var dismiss
@State private var isShortcutGuidesPresented = false

var body: some View {
ZStack(alignment: .top) {
Expand Down Expand Up @@ -89,7 +90,7 @@ struct ModeEditorView: View {
sectionLabel("RELEASE")
.padding(.top, 60)

Text("This mode uses the paired anchor and keeps its session state on this iPhone.")
Text("This mode uses the paired anchor and keeps its session state on this iPhone. Turn on strict mode if you want the harder-to-bypass version of this ritual.")
.font(.ancla(16))
.foregroundStyle(AnclaTheme.secondaryText)
.frame(maxWidth: .infinity, alignment: .leading)
Expand Down Expand Up @@ -133,6 +134,9 @@ struct ModeEditorView: View {
divider
.padding(.top, 24)

strictModePanel
.padding(.top, 40)

HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 6) {
Text("Set as Primary")
Expand Down Expand Up @@ -196,6 +200,10 @@ struct ModeEditorView: View {
.preferredColorScheme(.dark)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.hidden)
.sheet(isPresented: $isShortcutGuidesPresented) {
ShortcutGuidesSheet()
.presentationBackground(.clear)
}
}

private func sectionLabel(_ title: String) -> some View {
Expand All @@ -210,4 +218,70 @@ struct ModeEditorView: View {
.fill(AnclaTheme.panelStroke.opacity(0.6))
.frame(height: 1)
}

private var strictModePanel: some View {
VStack(alignment: .leading, spacing: 14) {
sectionLabel("STRICT MODE")

VStack(alignment: .leading, spacing: 14) {
HStack(alignment: .center) {
VStack(alignment: .leading, spacing: 6) {
Text("Use stricter mode copy")
.font(.ancla(16))
.foregroundStyle(AnclaTheme.primaryText)

Text("Highlight the stricter version of this mode and surface the Apple app shortcut guides that close easy loopholes.")
.font(.ancla(12))
.foregroundStyle(AnclaTheme.tertiaryText)
.frame(maxWidth: .infinity, alignment: .leading)
}

Spacer()

Toggle("", isOn: $viewModel.draftModeIsStrict)
.labelsHidden()
.tint(AnclaTheme.ctaFill)
}

if viewModel.draftModeIsStrict {
Text("Native Apple apps still need Shortcuts automations because iOS does not let Ancla hard-block every built-in app directly.")
.font(.ancla(13))
.foregroundStyle(AnclaTheme.secondaryText)
.frame(maxWidth: .infinity, alignment: .leading)

Button {
isShortcutGuidesPresented = true
} label: {
HStack {
Image(systemName: "bolt.horizontal.circle")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(AnclaTheme.primaryText)

Text("Review Apple app shortcut guides")
.font(.ancla(15, weight: .medium))
.foregroundStyle(AnclaTheme.primaryText)

Spacer()

Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(AnclaTheme.tertiaryText)
}
.padding(.horizontal, 16)
.frame(height: 52)
}
.buttonStyle(AnclaPressableButtonStyle())
}
}
.padding(16)
.background(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.fill(AnclaTheme.panelInteractive)
.overlay(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.stroke(AnclaTheme.panelStroke.opacity(0.75), lineWidth: 1)
)
)
}
}
}
50 changes: 50 additions & 0 deletions ios/ancla-app/native-apple-shortcut-guides.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Foundation

struct NativeAppleShortcutGuide: Identifiable, Equatable {
let id: String
let title: String
let apps: String
let summary: String
let steps: [String]
}

enum NativeAppleShortcutGuides {
static let guides: [NativeAppleShortcutGuide] = [
NativeAppleShortcutGuide(
id: "safari-settings",
title: "Safari and Settings",
apps: "Safari, Settings",
summary: "Use a personal automation that opens Ancla the moment Safari or Settings launches.",
steps: [
"Open Shortcuts and create a new Personal Automation.",
"Choose App, set Is Opened, and enable Run Immediately.",
"Select Safari or Settings as the trigger app.",
"Add an Open App action that launches Ancla.",
]
),
NativeAppleShortcutGuide(
id: "messages-mail",
title: "Messages and Mail",
apps: "Messages, Mail",
summary: "Bounce communication apps back into Ancla while a strict session is active.",
steps: [
"Duplicate the same Personal Automation flow for Messages or Mail.",
"Keep the trigger on Is Opened so the redirect happens instantly.",
"Use Open App -> Ancla as the only action.",
"Test each automation once before relying on it.",
]
),
NativeAppleShortcutGuide(
id: "phone-calendar",
title: "Phone and Calendar",
apps: "Phone, Calendar",
summary: "Cover the common Apple defaults that are easy to reach from habit.",
steps: [
"Create one automation per app so each shortcut stays obvious to audit.",
"Set the app trigger to Is Opened with Run Immediately.",
"Send the action straight into Ancla with Open App.",
"Use these only for sessions where the extra friction is worth it.",
]
),
]
}
Loading
Loading