diff --git a/.gitignore b/.gitignore
index 5ea09efa0..0a7e85039 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,7 @@ target
node_modules
dist-js
dist
-**/capabilities/schemas
\ No newline at end of file
+.idea
+.vscode
+.gradle
+**/capabilities/schemas
diff --git a/README.md b/README.md
index 444c4e99c..52ffce073 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@
| [shell](plugins/shell) | Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application. | ✅ | ✅ | ✅ | ? | ? |
| [single-instance](plugins/single-instance) | Ensure a single instance of your tauri app is running. | ✅ | ? | ✅ | ? | ? |
| [sql](plugins/sql) | Interface with SQL databases. | ✅ | ✅ | ✅ | ? | ? |
-| [store](plugins/store) | Persistent key value storage. | ✅ | ✅ | ✅ | ? | ? |
+| [store](plugins/store) | Persistent key value storage. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [stronghold](plugins/stronghold) | Encrypted, secure database. | ✅ | ✅ | ✅ | ? | ? |
| [updater](plugins/updater) | In-app updates for Tauri applications. | ✅ | ✅ | ✅ | ? | ? |
| [upload](plugins/upload) | Tauri plugin for file uploads through HTTP. | ✅ | ✅ | ✅ | ? | ? |
diff --git a/plugins/deep-link/src/api-iife.js b/plugins/deep-link/src/api-iife.js
index 6a37d7481..d46ebd7d1 100644
--- a/plugins/deep-link/src/api-iife.js
+++ b/plugins/deep-link/src/api-iife.js
@@ -1 +1 @@
-if("__TAURI__"in window){var __TAURI_PLUGIN_DEEPLINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function t(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}var r;async function _(e,r,_){const i="string"==typeof _?.target?{kind:"AnyLabel",label:_.target}:_?.target??{kind:"Any"};return t("plugin:event|listen",{event:e,target:i,handler:n(r)}).then((n=>async()=>async function(e,n){await t("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function i(){return await t("plugin:deep-link|get_current")}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WEBVIEW_CREATED="tauri://webview-created",e.WEBVIEW_FILE_DROP="tauri://file-drop",e.WEBVIEW_FILE_DROP_HOVER="tauri://file-drop-hover",e.WEBVIEW_FILE_DROP_CANCELLED="tauri://file-drop-cancelled"}(r||(r={})),e.getCurrent=i,e.onOpenUrl=async function(e){const n=await i();return null!=n&&e(n),await _("deep-link://new-url",(n=>e(n.payload)))},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEPLINK__})}
+if("__TAURI__"in window){var __TAURI_PLUGIN_DEEPLINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function t(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}var r;async function i(e,r,i){const _="string"==typeof i?.target?{kind:"AnyLabel",label:i.target}:i?.target??{kind:"Any"};return t("plugin:event|listen",{event:e,target:_,handler:n(r)}).then((n=>async()=>async function(e,n){await t("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function _(){return await t("plugin:deep-link|get_current")}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WEBVIEW_CREATED="tauri://webview-created",e.FILE_DROP="tauri://file-drop",e.FILE_DROP_HOVER="tauri://file-drop-hover",e.FILE_DROP_CANCELLED="tauri://file-drop-cancelled"}(r||(r={})),e.getCurrent=_,e.onOpenUrl=async function(e){const n=await _();return null!=n&&e(n),await i("deep-link://new-url",(n=>e(n.payload)))},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEPLINK__})}
diff --git a/plugins/store/android/build.gradle.kts b/plugins/store/android/build.gradle.kts
new file mode 100644
index 000000000..f7d161916
--- /dev/null
+++ b/plugins/store/android/build.gradle.kts
@@ -0,0 +1,40 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "app.tauri.store"
+ compileSdk = 33
+
+ defaultConfig {
+ minSdk = 19
+ targetSdk = 33
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+}
+
+dependencies {
+ implementation("androidx.core:core-ktx:1.9.0")
+ implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
+ implementation(project(":tauri-android"))
+}
diff --git a/plugins/store/android/proguard-rules.pro b/plugins/store/android/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/plugins/store/android/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/plugins/store/android/settings.gradle b/plugins/store/android/settings.gradle
new file mode 100644
index 000000000..14a752e43
--- /dev/null
+++ b/plugins/store/android/settings.gradle
@@ -0,0 +1,2 @@
+include ':tauri-android'
+project(':tauri-android').projectDir = new File('./.tauri/tauri-api')
diff --git a/plugins/store/android/src/main/AndroidManifest.xml b/plugins/store/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..9a40236b9
--- /dev/null
+++ b/plugins/store/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/plugins/store/android/src/main/java/StorePlugin.kt b/plugins/store/android/src/main/java/StorePlugin.kt
new file mode 100644
index 000000000..8389661ff
--- /dev/null
+++ b/plugins/store/android/src/main/java/StorePlugin.kt
@@ -0,0 +1,50 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+package app.tauri.store
+
+import android.app.Activity
+import app.tauri.annotation.Command
+import app.tauri.annotation.TauriPlugin
+import app.tauri.plugin.Invoke
+import app.tauri.plugin.Plugin
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import java.io.File
+
+@TauriPlugin
+class StorePlugin(private val activity: Activity) : Plugin(activity) {
+ @Command
+ fun load(invoke: Invoke) {
+ try {
+ val path = invoke.parseArgs(String::class.java)
+ val file = File(activity.applicationContext.getExternalFilesDir(null), path)
+
+ invoke.resolveObject(ObjectMapper().readTree(file))
+ } catch (ex: Exception) {
+ invoke.reject(ex.message)
+ }
+ }
+
+ @Command
+ fun save(invoke: Invoke) {
+ try {
+ val args = invoke.parseArgs(JsonNode::class.java)
+ val path = args.get("store").asText()
+ val cache = args.get("cache")
+ val file = File(activity.applicationContext.getExternalFilesDir(null), path)
+
+ if (!file.exists()) {
+ file.parentFile?.mkdirs()
+ file.createNewFile()
+ }
+
+ file.writeText(cache.toString())
+
+ invoke.resolve()
+ } catch (ex: Exception) {
+ invoke.reject(ex.message)
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/store/build.rs b/plugins/store/build.rs
index 30ed3968c..140b4a85b 100644
--- a/plugins/store/build.rs
+++ b/plugins/store/build.rs
@@ -8,5 +8,8 @@ const COMMANDS: &[&str] = &[
];
fn main() {
- tauri_plugin::Builder::new(COMMANDS).build();
+ tauri_plugin::Builder::new(COMMANDS)
+ .android_path("android")
+ .ios_path("ios")
+ .build();
}
diff --git a/plugins/store/ios/Package.resolved b/plugins/store/ios/Package.resolved
new file mode 100644
index 000000000..5f998e0e6
--- /dev/null
+++ b/plugins/store/ios/Package.resolved
@@ -0,0 +1,16 @@
+{
+ "object": {
+ "pins": [
+ {
+ "package": "SwiftRs",
+ "repositoryURL": "https://github.com/Brendonovich/swift-rs",
+ "state": {
+ "branch": null,
+ "revision": "b5ed223fcdab165bc21219c1925dc1e77e2bef5e",
+ "version": "1.0.6"
+ }
+ }
+ ]
+ },
+ "version": 1
+}
diff --git a/plugins/store/ios/Package.swift b/plugins/store/ios/Package.swift
new file mode 100644
index 000000000..fdf5f69a8
--- /dev/null
+++ b/plugins/store/ios/Package.swift
@@ -0,0 +1,33 @@
+// swift-tools-version:5.3
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+import PackageDescription
+
+let package = Package(
+ name: "tauri-plugin-store",
+ platforms: [
+ .iOS(.v13),
+ ],
+ products: [
+ // Products define the executables and libraries a package produces, and make them visible to other packages.
+ .library(
+ name: "tauri-plugin-store",
+ type: .static,
+ targets: ["tauri-plugin-store"]),
+ ],
+ dependencies: [
+ .package(name: "Tauri", path: "../.tauri/tauri-api")
+ ],
+ targets: [
+ // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+ // Targets can depend on other targets in this package, and on products in packages this package depends on.
+ .target(
+ name: "tauri-plugin-store",
+ dependencies: [
+ .byName(name: "Tauri")
+ ],
+ path: "Sources")
+ ]
+)
diff --git a/plugins/store/ios/Sources/StorePlugin.swift b/plugins/store/ios/Sources/StorePlugin.swift
new file mode 100644
index 000000000..4f651a5e2
--- /dev/null
+++ b/plugins/store/ios/Sources/StorePlugin.swift
@@ -0,0 +1,217 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+import Foundation
+
+import SwiftRs
+import Tauri
+import UIKit
+import WebKit
+
+
+struct SaveStore: Codable {
+ let store: String
+ let cache: [String: JSON]
+}
+
+class StorePlugin: Plugin {
+ @objc public func save(_ invoke: Invoke) throws {
+ do {
+ let args = try invoke.parseArgs(SaveStore.self)
+ let store = args.store
+ let cache = args.cache
+ let fileURL = getUrlFromPath(path: store, createDirs: true)
+
+ try JSONEncoder().encode(cache).write(to: fileURL)
+ invoke.resolve()
+ } catch {
+ invoke.reject(error.localizedDescription)
+ }
+ }
+
+ @objc public func load(_ invoke: Invoke) throws {
+ do {
+ let path = try invoke.parseArgs(String.self)
+ let fileURL = getUrlFromPath(path: path, createDirs: false)
+ let data = try String(contentsOf: fileURL)
+ let passData = dictionary(text: data)
+
+ invoke.resolve(passData)
+ } catch {
+ invoke.reject(error.localizedDescription)
+ }
+ }
+
+ func dictionary(text: String) -> [String: Any?] {
+ if let data = text.data(using: .utf8) {
+ do {
+ return try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
+ } catch {
+ fatalError(error.localizedDescription)
+ }
+ }
+
+ return [:]
+ }
+
+ func getUrlFromPath(path: String, createDirs: Bool) -> URL {
+ do {
+ var url = try FileManager.default
+ .url(
+ for: .applicationSupportDirectory,
+ in: .userDomainMask,
+ appropriateFor: nil,
+ create: true
+ )
+ let components = path.split(separator: "/").map { element in String(element) }
+
+ if components.count == 1 {
+ return url.appendPath(path: path, isDirectory: false)
+ }
+
+ for i in 0.. 1 && createDirs {
+ try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
+ }
+
+ url = url.appendPath(path: components.last!, isDirectory: false)
+
+ return url
+ } catch {
+ fatalError(error.localizedDescription)
+ }
+ }
+}
+
+
+@_cdecl("init_plugin_store")
+func initPlugin() -> Plugin {
+ return StorePlugin()
+}
+
+private extension URL {
+ func appendPath(path: String, isDirectory: Bool) -> URL {
+ if #available(iOS 16.0, *) {
+ return self.appending(path: path, directoryHint: isDirectory ? .isDirectory : .notDirectory)
+ } else {
+ return self.appendingPathComponent(path, isDirectory: isDirectory)
+ }
+ }
+}
+
+public enum JSON : Codable {
+ case null
+ case number(NSNumber)
+ case string(String)
+ case array([JSON])
+ case bool(Bool)
+ case dictionary([String : JSON])
+
+ public var value: Any? {
+ switch self {
+ case .null: return nil
+ case .number(let number): return number
+ case .string(let string): return string
+ case .bool(let bool): return bool
+ case .array(let array): return array.map { $0.value }
+ case .dictionary(let dictionary): return dictionary.mapValues { $0.value }
+ }
+ }
+
+ public init?(_ value: Any?) {
+ guard let value = value else {
+ self = .null
+ return
+ }
+
+ if let bool = value as? Bool {
+ self = .bool(bool)
+ } else if let int = value as? Int {
+ self = .number(NSNumber(value: int))
+ } else if let double = value as? Double {
+ self = .number(NSNumber(value: double))
+ } else if let string = value as? String {
+ self = .string(string)
+ } else if let array = value as? [Any] {
+ var mapped = [JSON]()
+ for inner in array {
+ guard let inner = JSON(inner) else {
+ return nil
+ }
+
+ mapped.append(inner)
+ }
+
+ self = .array(mapped)
+ } else if let dictionary = value as? [String : Any] {
+ var mapped = [String : JSON]()
+ for (key, inner) in dictionary {
+ guard let inner = JSON(inner) else {
+ return nil
+ }
+
+ mapped[key] = inner
+ }
+
+ self = .dictionary(mapped)
+ } else {
+ return nil
+ }
+ }
+
+ public init(from decoder: Decoder) throws {
+ let container = try decoder.singleValueContainer()
+ guard !container.decodeNil() else {
+ self = .null
+ return
+ }
+
+ if let bool = try container.decodeIfMatched(Bool.self) {
+ self = .bool(bool)
+ } else if let int = try container.decodeIfMatched(Int.self) {
+ self = .number(NSNumber(value: int))
+ } else if let double = try container.decodeIfMatched(Double.self) {
+ self = .number(NSNumber(value: double))
+ } else if let string = try container.decodeIfMatched(String.self) {
+ self = .string(string)
+ } else if let array = try container.decodeIfMatched([JSON].self) {
+ self = .array(array)
+ } else if let dictionary = try container.decodeIfMatched([String : JSON].self) {
+ self = .dictionary(dictionary)
+ } else {
+ throw DecodingError.typeMismatch(JSON.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unable to decode JSON as any of the possible types."))
+ }
+ }
+
+ public func encode(to encoder: Encoder) throws {
+ var container = encoder.singleValueContainer()
+
+ switch self {
+ case .null: try container.encodeNil()
+ case .bool(let bool): try container.encode(bool)
+ case .number(let number):
+ if number.objCType.pointee == 0x64 /* 'd' */ {
+ try container.encode(number.doubleValue)
+ } else {
+ try container.encode(number.intValue)
+ }
+ case .string(let string): try container.encode(string)
+ case .array(let array): try container.encode(array)
+ case .dictionary(let dictionary): try container.encode(dictionary)
+ }
+ }
+}
+
+fileprivate extension SingleValueDecodingContainer {
+ func decodeIfMatched(_ type: T.Type) throws -> T? {
+ do {
+ return try self.decode(T.self)
+ } catch DecodingError.typeMismatch {
+ return nil
+ }
+ }
+}
diff --git a/plugins/store/src/desktop.rs b/plugins/store/src/desktop.rs
new file mode 100644
index 000000000..3e98080e5
--- /dev/null
+++ b/plugins/store/src/desktop.rs
@@ -0,0 +1,49 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::Error;
+use crate::Runtime;
+use crate::Store;
+use std::fs::create_dir_all;
+use std::fs::read;
+use std::fs::File;
+use std::io::Write;
+use tauri::Manager;
+
+#[cfg(desktop)]
+impl Store {
+ pub fn save(&self) -> Result<(), Error> {
+ let app_dir = self
+ .app
+ .path()
+ .app_data_dir()
+ .expect("failed to resolve app dir");
+ let store_path = app_dir.join(&self.path);
+
+ create_dir_all(store_path.parent().expect("invalid store path"))?;
+
+ let bytes = (self.serialize)(&self.cache).map_err(Error::Serialize)?;
+ let mut f = File::create(&store_path)?;
+ f.write_all(&bytes)?;
+
+ Ok(())
+ }
+
+ /// Update the store from the on-disk state
+ pub fn load(&mut self) -> Result<(), Error> {
+ let app_dir = self
+ .app
+ .path()
+ .app_data_dir()
+ .expect("failed to resolve app dir");
+ let store_path = app_dir.join(&self.path);
+
+ let bytes = read(store_path)?;
+
+ self.cache
+ .extend((self.deserialize)(&bytes).map_err(Error::Deserialize)?);
+
+ Ok(())
+ }
+}
diff --git a/plugins/store/src/error.rs b/plugins/store/src/error.rs
index 0a04bb092..d8ce9bb51 100644
--- a/plugins/store/src/error.rs
+++ b/plugins/store/src/error.rs
@@ -5,10 +5,19 @@
use serde::{Serialize, Serializer};
use std::path::PathBuf;
+pub type Result = std::result::Result;
+
/// The error types.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
+ #[cfg(mobile)]
+ #[error(transparent)]
+ PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
+ /// Mobile plugin handled is not initialized, Probably [`StoreBuilder::mobile_plugin_handle`] was not called.
+ #[cfg(mobile)]
+ #[error("Mobile plugin handled is not initialized, Perhaps you forgot to call StoreBuilder::mobile_plugin_handle")]
+ MobilePluginHandleUnInitialized,
#[error("Failed to serialize store. {0}")]
Serialize(Box),
#[error("Failed to deserialize store. {0}")]
diff --git a/plugins/store/src/lib.rs b/plugins/store/src/lib.rs
index f76752f8f..700b058e4 100644
--- a/plugins/store/src/lib.rs
+++ b/plugins/store/src/lib.rs
@@ -11,7 +11,7 @@
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
-pub use error::Error;
+pub use error::{Error, Result};
use log::warn;
use serde::Serialize;
pub use serde_json::Value as JsonValue;
@@ -29,6 +29,18 @@ use tauri::{
mod error;
mod store;
+#[cfg(mobile)]
+mod mobile;
+#[cfg(mobile)]
+use crate::plugin::PluginHandle;
+#[cfg(target_os = "android")]
+const PLUGIN_IDENTIFIER: &str = "app.tauri.store";
+#[cfg(target_os = "ios")]
+tauri::ios_plugin_binding!(init_plugin_store);
+
+#[cfg(desktop)]
+mod desktop;
+
#[derive(Serialize, Clone)]
struct ChangePayload<'a> {
path: &'a Path,
@@ -36,18 +48,20 @@ struct ChangePayload<'a> {
value: &'a JsonValue,
}
-#[derive(Default)]
-pub struct StoreCollection {
+struct StoreCollection {
stores: Mutex>>,
frozen: bool,
+
+ #[cfg(mobile)]
+ mobile_plugin_handle: PluginHandle,
}
-pub fn with_store) -> Result>(
+fn with_store) -> Result>(
app: AppHandle,
collection: State<'_, StoreCollection>,
path: impl AsRef,
f: F,
-) -> Result {
+) -> Result {
let mut stores = collection.stores.lock().expect("mutex poisoned");
let path = path.as_ref();
@@ -55,7 +69,17 @@ pub fn with_store) -> Result>(
if collection.frozen {
return Err(Error::NotFound(path.to_path_buf()));
}
- let mut store = StoreBuilder::new(path).build(app);
+
+ #[allow(unused_mut)]
+ let mut builder = StoreBuilder::new(path);
+
+ #[cfg(mobile)]
+ {
+ builder = builder.mobile_plugin_handle(collection.mobile_plugin_handle.clone());
+ }
+
+ let mut store = builder.build(app);
+
// ignore loading errors, just use the default
if let Err(err) = store.load() {
warn!(
@@ -78,7 +102,7 @@ async fn set(
path: PathBuf,
key: String,
value: JsonValue,
-) -> Result<(), Error> {
+) -> Result<()> {
with_store(app, stores, path, |store| store.insert(key, value))
}
@@ -88,7 +112,7 @@ async fn get(
stores: State<'_, StoreCollection>,
path: PathBuf,
key: String,
-) -> Result