Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app-shortcuts): add configuration options #459

Merged
merged 6 commits into from
Mar 19, 2025
Merged
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
5 changes: 5 additions & 0 deletions .changeset/many-ghosts-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@capawesome/capacitor-app-shortcuts': minor
---

feat: add configuration option
43 changes: 43 additions & 0 deletions packages/app-shortcuts/README.md
Original file line number Diff line number Diff line change
@@ -9,6 +9,49 @@ npm install @capawesome/capacitor-app-shortcuts
npx cap sync
```

## Configuration

<docgen-config>
<!--Update the source file JSDoc comments and rerun docgen to update the docs below-->

| Prop | Type | Description | Since |
| --------------- | ----------------------- | ------------------------------------------------------------------------------------------- | ----- |
| **`shortcuts`** | <code>Shortcut[]</code> | The list of app shortcuts that should be set by default. Only available on Android and iOS. | 7.2.0 |

### Examples

In `capacitor.config.json`:

```json
{
"plugins": {
"AppShortcuts": {
"shortcuts": [{ id: 'feedback', title: 'Feedback' }]
}
}
}
```

In `capacitor.config.ts`:

```ts
/// <reference types="@capawesome/capacitor-app-shortcuts" />

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
plugins: {
AppShortcuts: {
shortcuts: [{ id: 'feedback', title: 'Feedback' }],
},
},
};

export default config;
```

</docgen-config>

### iOS

On iOS, you must add the following to your app's `AppDelegate.swift`:
Original file line number Diff line number Diff line change
@@ -2,7 +2,6 @@

import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import io.capawesome.capacitorjs.plugins.appshortcuts.classes.options.SetOptions;
@@ -16,8 +15,12 @@ public class AppShortcuts {

private final Context context;

public AppShortcuts(Context context) {
public AppShortcuts(Context context, AppShortcutsConfig config) {
this.context = context;
List<ShortcutInfoCompat> shortcuts = config.getShortcuts();
if (shortcuts != null) {
ShortcutManagerCompat.setDynamicShortcuts(context, shortcuts);
}
}

public void get(@NonNull NonEmptyCallback<Result> callback) {
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.capawesome.capacitorjs.plugins.appshortcuts;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.pm.ShortcutInfoCompat;
import java.util.List;

public class AppShortcutsConfig {

@Nullable
private List<ShortcutInfoCompat> shortcuts;

void setShortcuts(@NonNull List<ShortcutInfoCompat> shortcuts) {
this.shortcuts = shortcuts;
}

@Nullable
List<ShortcutInfoCompat> getShortcuts() {
return this.shortcuts;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package io.capawesome.capacitorjs.plugins.appshortcuts;

import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.graphics.drawable.IconCompat;
import com.getcapacitor.Bridge;
import com.getcapacitor.JSArray;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.json.JSONException;
import org.json.JSONObject;

@@ -18,4 +26,42 @@ public static HashMap<String, Object> createHashMapFromJSONObject(@NonNull JSONO
}
return map;
}

@NonNull
public static List<ShortcutInfoCompat> createShortcutInfoCompatList(JSArray shortcuts, Context context, Bridge bridge)
throws Exception {
ArrayList<ShortcutInfoCompat> shortcutInfoCompatList = new ArrayList<>();
List<JSONObject> shortcutsList = shortcuts.toList();
for (JSONObject shortcut : shortcutsList) {
HashMap<String, Object> shortcutMap = AppShortcutsHelper.createHashMapFromJSONObject(shortcut);
Object id = shortcutMap.get("id");
if (id == null) {
throw new Exception(AppShortcutsPlugin.ERROR_ID_MISSING);
}
Object title = shortcutMap.get("title");
if (title == null) {
throw new Exception(AppShortcutsPlugin.ERROR_TITLE_MISSING);
}
String description = (String) shortcutMap.get("description");
Object icon = shortcutMap.get("icon");

ShortcutInfoCompat.Builder shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, (String) id);
shortcutInfoCompat.setShortLabel((String) title);
if (description != null) {
shortcutInfoCompat.setLongLabel(description);
}
shortcutInfoCompat.setIntent(
new Intent(Intent.ACTION_VIEW, bridge.getIntentUri(), bridge.getContext(), bridge.getActivity().getClass()).putExtra(
AppShortcutsPlugin.INTENT_EXTRA_ITEM_NAME,
(String) id
)
);
if (icon != null) {
shortcutInfoCompat.setIcon(IconCompat.createWithResource(context, (int) icon));
}

shortcutInfoCompatList.add(shortcutInfoCompat.build());
}
return shortcutInfoCompatList;
}
}
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.getcapacitor.JSArray;
import com.getcapacitor.JSObject;
import com.getcapacitor.Logger;
import com.getcapacitor.Plugin;
@@ -15,6 +16,8 @@
import io.capawesome.capacitorjs.plugins.appshortcuts.interfaces.NonEmptyCallback;
import io.capawesome.capacitorjs.plugins.appshortcuts.interfaces.Result;
import java.util.Objects;
import org.json.JSONArray;
import org.json.JSONObject;

@CapacitorPlugin(name = "AppShortcuts")
public class AppShortcutsPlugin extends Plugin {
@@ -31,8 +34,7 @@ public class AppShortcutsPlugin extends Plugin {

@Override
public void load() {
super.load();
this.implementation = new AppShortcuts(getContext());
this.implementation = new AppShortcuts(getContext(), getAppShortcutsConfig());
}

@PluginMethod
@@ -128,4 +130,20 @@ protected void handleOnNewIntent(Intent intent) {
}
}
}

private AppShortcutsConfig getAppShortcutsConfig() {
AppShortcutsConfig config = new AppShortcutsConfig();
JSONObject configJSON = getConfig().getConfigJSON();
try {
JSONArray shortcutsJSON = configJSON.getJSONArray("shortcuts");
JSArray shortcuts = new JSArray();
for (int i = 0; i < shortcutsJSON.length(); i++) {
shortcuts.put(shortcutsJSON.get(i));
}
config.setShortcuts(AppShortcutsHelper.createShortcutInfoCompatList(shortcuts, getContext(), getBridge()));
} catch (Exception e) {
return config;
}
return config;
}
}
Original file line number Diff line number Diff line change
@@ -24,47 +24,11 @@ public SetOptions(@NonNull PluginCall call, @NonNull Context context, @NonNull B
if (shortcuts == null) {
throw new Exception(AppShortcutsPlugin.ERROR_SHORTCUTS_MISSING);
}
this.shortcuts = this.createShortcutInfoCompatList(shortcuts, context, bridge);
this.shortcuts = AppShortcutsHelper.createShortcutInfoCompatList(shortcuts, context, bridge);
}

@NonNull
public List<ShortcutInfoCompat> getShortcuts() {
return shortcuts;
}

private List<ShortcutInfoCompat> createShortcutInfoCompatList(JSArray shortcuts, Context context, Bridge bridge) throws Exception {
ArrayList<ShortcutInfoCompat> shortcutInfoCompatList = new ArrayList<>();
List<JSONObject> shortcutsList = shortcuts.toList();
for (JSONObject shortcut : shortcutsList) {
HashMap<String, Object> shortcutMap = AppShortcutsHelper.createHashMapFromJSONObject(shortcut);
Object id = shortcutMap.get("id");
if (id == null) {
throw new Exception(AppShortcutsPlugin.ERROR_ID_MISSING);
}
Object title = shortcutMap.get("title");
if (title == null) {
throw new Exception(AppShortcutsPlugin.ERROR_TITLE_MISSING);
}
String description = (String) shortcutMap.get("description");
Object icon = shortcutMap.get("icon");

ShortcutInfoCompat.Builder shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, (String) id);
shortcutInfoCompat.setShortLabel((String) title);
if (description != null) {
shortcutInfoCompat.setLongLabel(description);
}
shortcutInfoCompat.setIntent(
new Intent(Intent.ACTION_VIEW, bridge.getIntentUri(), bridge.getContext(), bridge.getActivity().getClass()).putExtra(
AppShortcutsPlugin.INTENT_EXTRA_ITEM_NAME,
(String) id
)
);
if (icon != null) {
shortcutInfoCompat.setIcon(IconCompat.createWithResource(context, (int) icon));
}

shortcutInfoCompatList.add(shortcutInfoCompat.build());
}
return shortcutInfoCompatList;
}
}
8 changes: 6 additions & 2 deletions packages/app-shortcuts/example/capacitor.config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"appId": "com.example.plugin",
"appName": "example",
"bundledWebRuntime": false,
"webDir": "dist"
"webDir": "dist",
"plugins": {
"AppShortcuts": {
"shortcuts": [{ "id": "feedback", "title": "Feedback" }]
}
}
}
2 changes: 1 addition & 1 deletion packages/app-shortcuts/example/ios/App/App/Info.plist
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>example</string>
<string>example</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
8 changes: 8 additions & 0 deletions packages/app-shortcuts/ios/Plugin.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -15,10 +15,12 @@
50ADFFA42020D75100D50D53 /* Capacitor.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ADFFA52020D75100D50D53 /* Capacitor.framework */; };
50E1A94820377CB70090CE1A /* AppShortcutsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E1A94720377CB70090CE1A /* AppShortcutsPlugin.swift */; };
7C23F4112D1434F80062E466 /* ClickEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C23F4102D1434F00062E466 /* ClickEvent.swift */; };
7CA40E142D8A110500FE1CC7 /* AppShortcutsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CA40E132D8A10FA00FE1CC7 /* AppShortcutsConfig.swift */; };
7CC437E92D0CCF94007934A8 /* GetResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC437E82D0CCF7A007934A8 /* GetResult.swift */; };
7CC437EC2D0CE897007934A8 /* SetOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC437EB2D0CE892007934A8 /* SetOptions.swift */; };
7CC437F52D0E3096007934A8 /* CustomError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC437F42D0E3096007934A8 /* CustomError.swift */; };
7CC437F82D0E30A0007934A8 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC437F62D0E30A0007934A8 /* Result.swift */; };
7CC64AA22D8AB6A500DC7F80 /* AppShortcutsPluginHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC64AA12D8AB69900DC7F80 /* AppShortcutsPluginHelper.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
@@ -43,10 +45,12 @@
50E1A94720377CB70090CE1A /* AppShortcutsPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppShortcutsPlugin.swift; sourceTree = "<group>"; };
5E23F77F099397094342571A /* Pods-Plugin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig"; sourceTree = "<group>"; };
7C23F4102D1434F00062E466 /* ClickEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClickEvent.swift; sourceTree = "<group>"; };
7CA40E132D8A10FA00FE1CC7 /* AppShortcutsConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutsConfig.swift; sourceTree = "<group>"; };
7CC437E82D0CCF7A007934A8 /* GetResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetResult.swift; sourceTree = "<group>"; };
7CC437EB2D0CE892007934A8 /* SetOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetOptions.swift; sourceTree = "<group>"; };
7CC437F42D0E3096007934A8 /* CustomError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomError.swift; sourceTree = "<group>"; };
7CC437F62D0E30A0007934A8 /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = "<group>"; };
7CC64AA12D8AB69900DC7F80 /* AppShortcutsPluginHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutsPluginHelper.swift; sourceTree = "<group>"; };
91781294A431A2A7CC6EB714 /* Pods-Plugin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Plugin.release.xcconfig"; path = "Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig"; sourceTree = "<group>"; };
96ED1B6440D6672E406C8D19 /* Pods-PluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig"; sourceTree = "<group>"; };
F65BB2953ECE002E1EF3E424 /* Pods-PluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PluginTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig"; sourceTree = "<group>"; };
@@ -98,6 +102,8 @@
50ADFF8A201F53D600D50D53 /* Plugin */ = {
isa = PBXGroup;
children = (
7CC64AA12D8AB69900DC7F80 /* AppShortcutsPluginHelper.swift */,
7CA40E132D8A10FA00FE1CC7 /* AppShortcutsConfig.swift */,
7CC437F72D0E30A0007934A8 /* Protocols */,
7CC437F32D0E308D007934A8 /* Enums */,
7CC437E62D0CCF63007934A8 /* Classes */,
@@ -367,6 +373,8 @@
50E1A94820377CB70090CE1A /* AppShortcutsPlugin.swift in Sources */,
7CC437E92D0CCF94007934A8 /* GetResult.swift in Sources */,
2F98D68224C9AAE500613A4C /* AppShortcuts.swift in Sources */,
7CA40E142D8A110500FE1CC7 /* AppShortcutsConfig.swift in Sources */,
7CC64AA22D8AB6A500DC7F80 /* AppShortcutsPluginHelper.swift in Sources */,
7CC437F82D0E30A0007934A8 /* Result.swift in Sources */,
7C23F4112D1434F80062E466 /* ClickEvent.swift in Sources */,
);
10 changes: 10 additions & 0 deletions packages/app-shortcuts/ios/Plugin/AppShortcuts.swift
Original file line number Diff line number Diff line change
@@ -2,6 +2,16 @@ import Foundation
import UIKit

@objc public class AppShortcuts: NSObject {
let config: AppShortcutsConfig

public init(_ config: AppShortcutsConfig) {
self.config = config
super.init()
if let configShortcuts = self.config.shortcuts {
self.set(shortcuts: configShortcuts, completion: { _ in })
}
}

@objc public func get(completion: @escaping (Result) -> Void) {
let application = UIApplication.shared

5 changes: 5 additions & 0 deletions packages/app-shortcuts/ios/Plugin/AppShortcutsConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import UIKit

public struct AppShortcutsConfig {
var shortcuts: [UIApplicationShortcutItem]?
}
25 changes: 21 additions & 4 deletions packages/app-shortcuts/ios/Plugin/AppShortcutsPlugin.swift
Original file line number Diff line number Diff line change
@@ -20,19 +20,24 @@ public class AppShortcutsPlugin: CAPPlugin, CAPBridgedPlugin {
public let eventClick = "click"
public let tag = "AppShortcuts"

private let implementation = AppShortcuts()
private var implementation: AppShortcuts?

override public init() {
super.init()
NotificationCenter.default.addObserver(self, selector: #selector(self.handleAppShortcutNotification), name: NSNotification.Name(AppShortcutsPlugin.notificationName), object: nil)
}

override public func load() {
super.load()
self.implementation = AppShortcuts(getAppShortcutConfig())
}

deinit {
NotificationCenter.default.removeObserver(self)
}

@objc func get(_ call: CAPPluginCall) {
implementation.get(completion: { result in
implementation?.get(completion: { result in
if let result = result.toJSObject() as? JSObject {
self.resolveCall(call, result)
}
@@ -42,7 +47,7 @@ public class AppShortcutsPlugin: CAPPlugin, CAPBridgedPlugin {
@objc func set(_ call: CAPPluginCall) {
do {
let options = try SetOptions(call: call)
implementation.set(shortcuts: options.getShortcuts, completion: { error in
implementation?.set(shortcuts: options.getShortcuts, completion: { error in
if let error = error {
self.rejectCall(call, error)
} else {
@@ -55,7 +60,7 @@ public class AppShortcutsPlugin: CAPPlugin, CAPBridgedPlugin {
}

@objc func clear(_ call: CAPPluginCall) {
implementation.clear(completion: { error in
implementation?.clear(completion: { error in
if let error = error {
self.rejectCall(call, error)
} else {
@@ -90,4 +95,16 @@ public class AppShortcutsPlugin: CAPPlugin, CAPBridgedPlugin {
call.resolve()
}
}

private func getAppShortcutConfig() -> AppShortcutsConfig {
var config = AppShortcutsConfig()
if let shortcuts = getConfig().getArray("shortcuts") as? [JSObject] {
do {
config.shortcuts = try AppShortcutsPluginHelper.getShortcutItemsFromJSArray(shortcuts: shortcuts)
} catch {
return config
}
}
return config
}
}
34 changes: 34 additions & 0 deletions packages/app-shortcuts/ios/Plugin/AppShortcutsPluginHelper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Capacitor

public class AppShortcutsPluginHelper {
static func getShortcutItemsFromJSArray(shortcuts: [JSObject]) throws -> [UIApplicationShortcutItem] {
return try shortcuts.map { shortcut in
guard let type = shortcut["id"] as? String, !type.isEmpty else {
throw CustomError.idMissing
}
guard let title = shortcut["title"] as? String, !title.isEmpty else {
throw CustomError.titleMissing
}
let description = shortcut["description"] as? String
let iconInt = shortcut["icon"] as? Int
let iconString = shortcut["icon"] as? String

var icon: UIApplicationShortcutIcon?

if let iconString = iconString {
icon = UIImage(systemName: iconString) != nil
? UIApplicationShortcutIcon(systemImageName: iconString)
: UIApplicationShortcutIcon(templateImageName: iconString)
} else if let iconInt = iconInt, let iconType = UIApplicationShortcutIcon.IconType(rawValue: iconInt) {
icon = UIApplicationShortcutIcon(type: iconType)
}

return UIApplicationShortcutItem(
type: type,
localizedTitle: title,
localizedSubtitle: description,
icon: icon
)
}
}
}
Original file line number Diff line number Diff line change
@@ -8,41 +8,10 @@ import Capacitor
call.reject(CustomError.shortcutsMissing.localizedDescription)
return
}
self.shortcuts = try SetOptions.getShortcutItemsFromJSArray(shortcuts: shortcuts)
self.shortcuts = try AppShortcutsPluginHelper.getShortcutItemsFromJSArray(shortcuts: shortcuts)
}

public var getShortcuts: [UIApplicationShortcutItem] {
return shortcuts
}

private static func getShortcutItemsFromJSArray(shortcuts: [JSObject]) throws -> [UIApplicationShortcutItem] {
return try shortcuts.map { shortcut in
guard let type = shortcut["id"] as? String, !type.isEmpty else {
throw CustomError.idMissing
}
guard let title = shortcut["title"] as? String, !title.isEmpty else {
throw CustomError.titleMissing
}
let description = shortcut["description"] as? String
let iconInt = shortcut["icon"] as? Int
let iconString = shortcut["icon"] as? String

var icon: UIApplicationShortcutIcon?

if let iconString = iconString {
icon = UIImage(systemName: iconString) != nil
? UIApplicationShortcutIcon(systemImageName: iconString)
: UIApplicationShortcutIcon(templateImageName: iconString)
} else if let iconInt = iconInt, let iconType = UIApplicationShortcutIcon.IconType(rawValue: iconInt) {
icon = UIApplicationShortcutIcon(type: iconType)
}

return UIApplicationShortcutItem(
type: type,
localizedTitle: title,
localizedSubtitle: description,
icon: icon
)
}
}
}
18 changes: 18 additions & 0 deletions packages/app-shortcuts/src/definitions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
/// <reference types="@capacitor/cli" />

import type { PluginListenerHandle } from '@capacitor/core';

declare module '@capacitor/cli' {
export interface PluginsConfig {
AppShortcuts?: {
/**
* The list of app shortcuts that should be set by default.
*
* Only available on Android and iOS.
*
* @since 7.2.0
* @example [{ id: 'feedback', title: 'Feedback' }]
*/
shortcuts?: Shortcut[];
};
}
}

export interface AppShortcutsPlugin {
/**
* Remove all app shortcuts.