Skip to content

Commit 7f8f082

Browse files
committed
Add App Shortcuts with navigation intents to the Jetpack app
Four App Intents expose new post, notifications, stats, and Reader navigation through Shortcuts, Spotlight, and Siri. Existing compatible app strings are reused for localization, while the remaining App Intent text and invocation phrases use their English source values until the shared String Catalog pipeline is ready.
1 parent 5778339 commit 7f8f082

10 files changed

Lines changed: 482 additions & 0 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import AppIntents
2+
import Foundation
3+
4+
/// Errors surfaced to the system when an open intent cannot proceed.
5+
enum AppIntentOpenError: Error, CustomLocalizedStringResourceConvertible {
6+
case notLoggedIn
7+
case siteNotFound
8+
9+
var localizedStringResource: LocalizedStringResource {
10+
switch self {
11+
case .notLoggedIn:
12+
return LocalizedStringResource(
13+
"ios-appintents.openError.notLoggedIn",
14+
defaultValue: "Sign in to the app first to use this action.",
15+
table: "AppIntents",
16+
comment:
17+
"Error shown by Siri or the Shortcuts app when an App Intent runs while no account is signed in."
18+
)
19+
case .siteNotFound:
20+
return LocalizedStringResource(
21+
"ios-appintents.openError.siteNotFound",
22+
defaultValue: "The site could not be found.",
23+
table: "AppIntents",
24+
comment:
25+
"Error shown by Siri or the Shortcuts app when the site an App Intent should act on no longer exists."
26+
)
27+
}
28+
}
29+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import AppIntents
2+
3+
/// Surfaces the navigation intents in the Shortcuts app and Siri without any user setup.
4+
struct JetpackAppShortcuts: AppShortcutsProvider {
5+
static var appShortcuts: [AppShortcut] {
6+
AppShortcut(
7+
intent: NewPostIntent(),
8+
phrases: [
9+
"Create a new post in \(.applicationName)",
10+
"Write a new post in \(.applicationName)",
11+
"New \(.applicationName) post"
12+
],
13+
shortTitle: LocalizedStringResource(
14+
"New Post",
15+
defaultValue: "New Post",
16+
table: "Localizable",
17+
comment: "Short title of the New Post shortcut tile in Spotlight and the Shortcuts app."
18+
),
19+
systemImageName: "square.and.pencil"
20+
)
21+
AppShortcut(
22+
intent: OpenNotificationsIntent(),
23+
phrases: [
24+
"Open my \(.applicationName) notifications",
25+
"Show my \(.applicationName) notifications"
26+
],
27+
shortTitle: LocalizedStringResource(
28+
"Notifications",
29+
defaultValue: "Notifications",
30+
table: "Localizable",
31+
comment: "Short title of the Notifications shortcut tile in Spotlight and the Shortcuts app."
32+
),
33+
systemImageName: "bell"
34+
)
35+
AppShortcut(
36+
intent: OpenStatsIntent(),
37+
phrases: [
38+
"Open my \(.applicationName) stats",
39+
"Show my site stats in \(.applicationName)"
40+
],
41+
shortTitle: LocalizedStringResource(
42+
"Stats",
43+
defaultValue: "Stats",
44+
table: "Localizable",
45+
comment: "Short title of the Stats shortcut tile in Spotlight and the Shortcuts app."
46+
),
47+
systemImageName: "chart.bar"
48+
)
49+
AppShortcut(
50+
intent: OpenReaderIntent(),
51+
phrases: [
52+
"Open the \(.applicationName) Reader",
53+
"Open Reader in \(.applicationName)"
54+
],
55+
shortTitle: LocalizedStringResource(
56+
"Reader",
57+
defaultValue: "Reader",
58+
table: "Localizable",
59+
comment: "Short title of the Reader shortcut tile in Spotlight and the Shortcuts app."
60+
),
61+
systemImageName: "book"
62+
)
63+
}
64+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import AppIntents
2+
3+
/// Opens the post editor for the last used site.
4+
struct NewPostIntent: AppIntent {
5+
static let title = LocalizedStringResource(
6+
"New Post",
7+
defaultValue: "New Post",
8+
table: "Localizable",
9+
comment: "Title of the App Intent that opens the post editor. Shown in the Shortcuts app and Spotlight."
10+
)
11+
static let description = IntentDescription(
12+
LocalizedStringResource(
13+
"ios-appintents.newPost.description",
14+
defaultValue: "Opens the editor to write a new post.",
15+
table: "AppIntents",
16+
comment: "Description of the App Intent that opens the post editor. Shown in the Shortcuts app."
17+
)
18+
)
19+
static let openAppWhenRun = true
20+
21+
@MainActor
22+
func perform() async throws -> some IntentResult {
23+
guard AccountHelper.isLoggedIn else {
24+
throw AppIntentOpenError.notLoggedIn
25+
}
26+
RootViewCoordinator.sharedPresenter.showPostEditor(animated: false)
27+
return .result()
28+
}
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import AppIntents
2+
3+
/// Opens the Notifications tab.
4+
struct OpenNotificationsIntent: AppIntent {
5+
static let title = LocalizedStringResource(
6+
"ios-appintents.openNotifications.title",
7+
defaultValue: "Open Notifications",
8+
table: "AppIntents",
9+
comment: "Title of the App Intent that opens the Notifications tab. Shown in the Shortcuts app and Spotlight."
10+
)
11+
static let description = IntentDescription(
12+
LocalizedStringResource(
13+
"ios-appintents.openNotifications.description",
14+
defaultValue: "Opens your notifications.",
15+
table: "AppIntents",
16+
comment: "Description of the App Intent that opens the Notifications tab. Shown in the Shortcuts app."
17+
)
18+
)
19+
static let openAppWhenRun = true
20+
21+
@MainActor
22+
func perform() async throws -> some IntentResult {
23+
guard AccountHelper.isLoggedIn else {
24+
throw AppIntentOpenError.notLoggedIn
25+
}
26+
let presenter = RootViewCoordinator.sharedPresenter
27+
presenter.rootViewController.dismiss(animated: false)
28+
presenter.showNotificationsTab()
29+
return .result()
30+
}
31+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import AppIntents
2+
3+
/// Opens the Reader tab.
4+
struct OpenReaderIntent: AppIntent {
5+
static let title = LocalizedStringResource(
6+
"notifications.emptyState.buttonOpenReader",
7+
defaultValue: "Open Reader",
8+
table: "Localizable",
9+
comment: "Title of the App Intent that opens the Reader tab. Shown in the Shortcuts app and Spotlight."
10+
)
11+
static let description = IntentDescription(
12+
LocalizedStringResource(
13+
"ios-appintents.openReader.description",
14+
defaultValue: "Opens the Reader to browse blogs you follow.",
15+
table: "AppIntents",
16+
comment: "Description of the App Intent that opens the Reader tab. Shown in the Shortcuts app."
17+
)
18+
)
19+
static let openAppWhenRun = true
20+
21+
@MainActor
22+
func perform() async throws -> some IntentResult {
23+
guard AccountHelper.isLoggedIn else {
24+
throw AppIntentOpenError.notLoggedIn
25+
}
26+
let presenter = RootViewCoordinator.sharedPresenter
27+
presenter.rootViewController.dismiss(animated: false)
28+
presenter.showReader(path: nil)
29+
return .result()
30+
}
31+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import AppIntents
2+
import JetpackStatsWidgetsCore
3+
import WordPressData
4+
5+
/// Opens Stats for a chosen site, or the last used site when none is picked.
6+
struct OpenStatsIntent: AppIntent {
7+
static let title = LocalizedStringResource(
8+
"ios-appintents.openStats.title",
9+
defaultValue: "Open Stats",
10+
table: "AppIntents",
11+
comment: "Title of the App Intent that opens Stats for a site. Shown in the Shortcuts app and Spotlight."
12+
)
13+
static let description = IntentDescription(
14+
LocalizedStringResource(
15+
"ios-appintents.openStats.description",
16+
defaultValue: "Opens the stats for one of your sites.",
17+
table: "AppIntents",
18+
comment: "Description of the App Intent that opens Stats for a site. Shown in the Shortcuts app."
19+
)
20+
)
21+
static let openAppWhenRun = true
22+
23+
@Parameter(
24+
title: LocalizedStringResource(
25+
"ios-widget.ILcGmf",
26+
defaultValue: "Site",
27+
table: "Localizable",
28+
comment: "Label of the site parameter of the Open Stats App Intent. Shown in the Shortcuts app."
29+
)
30+
)
31+
var site: SiteEntity?
32+
33+
@MainActor
34+
func perform() async throws -> some IntentResult {
35+
guard AccountHelper.isLoggedIn else {
36+
throw AppIntentOpenError.notLoggedIn
37+
}
38+
guard let blog = Blog.forAppIntent(siteIdentifier: site?.id, in: ContextManager.shared.mainContext) else {
39+
throw AppIntentOpenError.siteNotFound
40+
}
41+
let presenter = RootViewCoordinator.sharedPresenter
42+
presenter.rootViewController.dismiss(animated: false)
43+
presenter.showStats(for: blog, source: .shortcut)
44+
return .result()
45+
}
46+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
{
2+
"sourceLanguage" : "en",
3+
"strings" : {
4+
"ios-appintents.newPost.description" : {
5+
"comment" : "Description of the App Intent that opens the post editor. Shown in the Shortcuts app.",
6+
"localizations" : {
7+
"en" : {
8+
"stringUnit" : {
9+
"state" : "translated",
10+
"value" : "Opens the editor to write a new post."
11+
}
12+
}
13+
}
14+
},
15+
"ios-appintents.openError.notLoggedIn" : {
16+
"comment" : "Error shown by Siri or the Shortcuts app when an App Intent runs while no account is signed in.",
17+
"localizations" : {
18+
"en" : {
19+
"stringUnit" : {
20+
"state" : "translated",
21+
"value" : "Sign in to the app first to use this action."
22+
}
23+
}
24+
}
25+
},
26+
"ios-appintents.openError.siteNotFound" : {
27+
"comment" : "Error shown by Siri or the Shortcuts app when the site an App Intent should act on no longer exists.",
28+
"localizations" : {
29+
"en" : {
30+
"stringUnit" : {
31+
"state" : "translated",
32+
"value" : "The site could not be found."
33+
}
34+
}
35+
}
36+
},
37+
"ios-appintents.openNotifications.description" : {
38+
"comment" : "Description of the App Intent that opens the Notifications tab. Shown in the Shortcuts app.",
39+
"localizations" : {
40+
"en" : {
41+
"stringUnit" : {
42+
"state" : "translated",
43+
"value" : "Opens your notifications."
44+
}
45+
}
46+
}
47+
},
48+
"ios-appintents.openNotifications.title" : {
49+
"comment" : "Title of the App Intent that opens the Notifications tab. Shown in the Shortcuts app and Spotlight.",
50+
"localizations" : {
51+
"en" : {
52+
"stringUnit" : {
53+
"state" : "translated",
54+
"value" : "Open Notifications"
55+
}
56+
}
57+
}
58+
},
59+
"ios-appintents.openReader.description" : {
60+
"comment" : "Description of the App Intent that opens the Reader tab. Shown in the Shortcuts app.",
61+
"localizations" : {
62+
"en" : {
63+
"stringUnit" : {
64+
"state" : "translated",
65+
"value" : "Opens the Reader to browse blogs you follow."
66+
}
67+
}
68+
}
69+
},
70+
"ios-appintents.openStats.description" : {
71+
"comment" : "Description of the App Intent that opens Stats for a site. Shown in the Shortcuts app.",
72+
"localizations" : {
73+
"en" : {
74+
"stringUnit" : {
75+
"state" : "translated",
76+
"value" : "Opens the stats for one of your sites."
77+
}
78+
}
79+
}
80+
},
81+
"ios-appintents.openStats.title" : {
82+
"comment" : "Title of the App Intent that opens Stats for a site. Shown in the Shortcuts app and Spotlight.",
83+
"localizations" : {
84+
"en" : {
85+
"stringUnit" : {
86+
"state" : "translated",
87+
"value" : "Open Stats"
88+
}
89+
}
90+
}
91+
}
92+
},
93+
"version" : "1.0"
94+
}

0 commit comments

Comments
 (0)