Skip to content
Draft
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
38 changes: 38 additions & 0 deletions apps/mobile/iosApp/ExportOptions.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Export options for a TestFlight / App Store build.

xcodebuild -exportArchive \
-archivePath build/Piro.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath build/export

teamID is read from the DEVELOPMENT_TEAM build setting, so it is deliberately absent here: keeping
it in one place (project.yml) means the two cannot disagree. Pass -allowProvisioningUpdates on the
archive step and Xcode manages the distribution profile itself, matching CODE_SIGN_STYLE: Automatic.
-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- TestFlight and the App Store are the same distribution channel; the build is promoted from
one to the other in App Store Connect, not re-exported. -->
<key>method</key>
<string>app-store-connect</string>

<key>signingStyle</key>
<string>automatic</string>

<!-- Symbols let App Store Connect symbolicate crash reports. Worth it: the alternative is
reading raw addresses out of a crash from a device you do not have. -->
<key>uploadSymbols</key>
<true/>

<!-- Bitcode is gone as of Xcode 14; stating it avoids a warning on some toolchains. -->
<key>compileBitcode</key>
<false/>

<!-- App Store builds are thinned by the store itself, so exporting a universal binary is right. -->
<key>stripSwiftSymbols</key>
<true/>
</dict>
</plist>
152 changes: 150 additions & 2 deletions apps/mobile/iosApp/Piro.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AE9E99BD1414C3B4E9B987B4"
BuildableName = "Piro.app"
BlueprintName = "Piro"
ReferencedContainer = "container:Piro.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AE9E99BD1414C3B4E9B987B4"
BuildableName = "Piro.app"
BlueprintName = "Piro"
ReferencedContainer = "container:Piro.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AE9E99BD1414C3B4E9B987B4"
BuildableName = "Piro.app"
BlueprintName = "Piro"
ReferencedContainer = "container:Piro.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AE9E99BD1414C3B4E9B987B4"
BuildableName = "Piro.app"
BlueprintName = "Piro"
ReferencedContainer = "container:Piro.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
wasCreatedForAppExtension = "YES"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "NO"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D1CCDB0F70F42F7329C71BCE"
BuildableName = "PiroNotificationService.appex"
BlueprintName = "PiroNotificationService"
ReferencedContainer = "container:Piro.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D1CCDB0F70F42F7329C71BCE"
BuildableName = "PiroNotificationService.appex"
BlueprintName = "PiroNotificationService"
ReferencedContainer = "container:Piro.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D1CCDB0F70F42F7329C71BCE"
BuildableName = "PiroNotificationService.appex"
BlueprintName = "PiroNotificationService"
ReferencedContainer = "container:Piro.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D1CCDB0F70F42F7329C71BCE"
BuildableName = "PiroNotificationService.appex"
BlueprintName = "PiroNotificationService"
ReferencedContainer = "container:Piro.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
7 changes: 5 additions & 2 deletions apps/mobile/iosApp/Piro/Auth/LoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct LoginView: View {

private var serverField: some View {
VStack(alignment: .leading, spacing: 4) {
TextField("Server URL (https://piro.example.com)", text: $session.serverURL)
TextField("Server URL", text: $session.serverURL)
.textContentType(.URL)
.keyboardType(.URL)
.textInputAutocapitalization(.never)
Expand All @@ -78,7 +78,10 @@ struct LoginView: View {
private var credentialFields: some View {
VStack(spacing: 12) {
TextField("Email", text: $session.email)
.textContentType(.username)
// .emailAddress rather than .username: it is what makes iOS offer the addresses from
// Contacts and the keyboard's own suggestions, on top of any saved credential. With
// .username the QuickType bar only ever proposes a stored login.
.textContentType(.emailAddress)
.keyboardType(.emailAddress)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
Expand Down
52 changes: 47 additions & 5 deletions apps/mobile/iosApp/Piro/OnCall/OnCallView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct OnCallView: View {

statusBanner
.padding(.top, 28)
.animation(.easeInOut(duration: 0.25), value: readiness)
Spacer()
Spacer()
}
Expand All @@ -31,21 +32,44 @@ struct OnCallView: View {
}
}

@ViewBuilder
private var statusBanner: some View {
Text(bannerText)
.font(.callout.weight(.medium))
.multilineTextAlignment(.center)
.foregroundStyle(bannerColor)
if readiness == .registering {
// While registering, the app genuinely does not know whether this device will be paged, so
// it shows a placeholder instead of a claim. The card keeps its shape and position, so the
// layout does not jump when the real answer arrives a moment later.
VStack(spacing: 8) {
SkeletonBar(width: 220)
SkeletonBar(width: 150)
}
.padding(16)
.frame(maxWidth: .infinity)
.glassCard(cornerRadius: 12, tint: bannerColor.opacity(0.12))
.glassCard(cornerRadius: 12, tint: PiroColors.muted(scheme).opacity(0.08))
.accessibilityElement(children: .ignore)
.accessibilityLabel("Arming this device to receive pages")
// VoiceOver should hear progress rather than silence, since the skeleton conveys nothing
// to a screen reader on its own.
.accessibilityAddTraits(.updatesFrequently)
.transition(.opacity)
} else {
Text(bannerText)
.font(.callout.weight(.medium))
.multilineTextAlignment(.center)
.foregroundStyle(bannerColor)
.padding(16)
.frame(maxWidth: .infinity)
.glassCard(cornerRadius: 12, tint: bannerColor.opacity(0.12))
.transition(.opacity)
}
}

/// The banner only promises pages when the device is actually registered with the backend — otherwise
/// it tells the truth (permission needed, still registering, or registration failed).
private var bannerText: String {
switch readiness {
case .registered: return "This device will receive critical pages, even on silent."
// Not rendered: .registering shows the skeleton above. Kept so the switch stays exhaustive
// and the string exists if the banner is ever reused somewhere without one.
case .registering: return "Arming this device to receive pages…"
case .needsPermission: return "Enable notifications so pages can reach you."
case .failed: return "This device isn't registered for pages yet — you may not be paged."
Expand All @@ -63,3 +87,21 @@ struct OnCallView: View {
}
}
}

// Previews for every readiness state, because the real ones are hard to catch: registration completes
// in well under a second, so the skeleton would otherwise only ever be seen by accident.
#Preview("Registering (skeleton)") {
OnCallView(userName: "aespinosa@heva.co", readiness: .registering)
}

#Preview("Registered") {
OnCallView(userName: "aespinosa@heva.co", readiness: .registered)
}

#Preview("Needs permission") {
OnCallView(userName: "aespinosa@heva.co", readiness: .needsPermission)
}

#Preview("Failed") {
OnCallView(userName: "aespinosa@heva.co", readiness: .failed)
}
21 changes: 21 additions & 0 deletions apps/mobile/iosApp/Piro/Piro.Release.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!--
Release entitlements, used for archives: TestFlight and the App Store.

TestFlight builds are Release builds, so they register against *production* APNs, not the sandbox
the simulator and Xcode runs use. Shipping `development` here is why pushes work in development and
then silently stop once the build comes from TestFlight.
-->
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>production</string>
<key>keychain-access-groups</key>
<array>
<!-- Shared with the notification service extension so it can read the push private key. The
$(AppIdentifierPrefix) prefix is the Team ID, filled in at build time. -->
<string>$(AppIdentifierPrefix)co.heva.piro</string>
</array>
</dict>
</plist>
15 changes: 14 additions & 1 deletion apps/mobile/iosApp/Piro/Piro.entitlements
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Debug entitlements. APNs sandbox, which is what a build run from Xcode registers against.

Release builds use Piro.Release.entitlements (aps-environment: production) instead, selected per
configuration in project.yml. This has to be a separate file rather than one value flipped by the
build: aps-environment is a fixed string in a plist, nothing rewrites it, and a TestFlight build
signed with `development` registers sandbox tokens that production APNs will not deliver to.
-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- APNs. `development` for debug builds; the release/distribution build flips this to `production`. -->
<key>aps-environment</key>
<string>development</string>
<key>keychain-access-groups</key>
<array>
<!-- Shared with the notification service extension so it can read the push private key. The
$(AppIdentifierPrefix) prefix is the Team ID, filled in at build time. -->
<string>$(AppIdentifierPrefix)co.heva.piro</string>
</array>
</dict>
</plist>
Loading
Loading