summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock368
-rw-r--r--app/Cargo.toml9
-rw-r--r--app/Tauri.toml5
-rw-r--r--app/src/lib.rs186
-rw-r--r--app/src/state.rs46
-rw-r--r--ui/src/root.tsx26
6 files changed, 614 insertions, 26 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4d667e3..508cba3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -504,9 +504,9 @@ dependencies = [
  "bitflags 2.8.0",
  "block",
  "cocoa-foundation",
- "core-foundation",
+ "core-foundation 0.10.0",
  "core-graphics",
- "foreign-types",
+ "foreign-types 0.5.0",
  "libc",
  "objc",
 ]
@@ -519,7 +519,7 @@ checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d"
 dependencies = [
  "bitflags 2.8.0",
  "block",
- "core-foundation",
+ "core-foundation 0.10.0",
  "core-graphics-types",
  "libc",
  "objc",
@@ -545,6 +545,26 @@ dependencies = [
 ]
 
 [[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.15",
+ "once_cell",
+ "tiny-keccak",
+]
+
+[[package]]
 name = "convert_case"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -562,6 +582,16 @@ dependencies = [
 
 [[package]]
 name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
 version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
@@ -583,9 +613,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
 dependencies = [
  "bitflags 2.8.0",
- "core-foundation",
+ "core-foundation 0.10.0",
  "core-graphics-types",
- "foreign-types",
+ "foreign-types 0.5.0",
  "libc",
 ]
 
@@ -596,7 +626,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
 dependencies = [
  "bitflags 2.8.0",
- "core-foundation",
+ "core-foundation 0.10.0",
  "libc",
 ]
 
@@ -634,6 +664,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 
 [[package]]
+name = "crunchy"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+
+[[package]]
 name = "crypto-common"
 version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -831,6 +867,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "dlv-list"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
+dependencies = [
+ "const-random",
+]
+
+[[package]]
 name = "dpi"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -887,6 +932,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
 
 [[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
 name = "endi"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1003,12 +1057,21 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
 [[package]]
 name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
+[[package]]
+name = "foreign-types"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
 dependencies = [
  "foreign-types-macros",
- "foreign-types-shared",
+ "foreign-types-shared 0.3.1",
 ]
 
 [[package]]
@@ -1024,6 +1087,12 @@ dependencies = [
 
 [[package]]
 name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "foreign-types-shared"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
@@ -1041,11 +1110,16 @@ dependencies = [
 name = "foxfleet"
 version = "0.1.0"
 dependencies = [
+ "reqwest",
  "serde",
  "serde_json",
  "tauri",
  "tauri-build",
+ "tauri-plugin-deep-link",
  "tauri-plugin-opener",
+ "tauri-plugin-single-instance",
+ "tokio",
+ "url",
 ]
 
 [[package]]
@@ -1456,6 +1530,25 @@ dependencies = [
 ]
 
 [[package]]
+name = "h2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap 2.7.1",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
 name = "hashbrown"
 version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1463,6 +1556,12 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
 [[package]]
 name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "hashbrown"
 version = "0.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
@@ -1554,6 +1653,7 @@ dependencies = [
  "bytes",
  "futures-channel",
  "futures-util",
+ "h2",
  "http",
  "http-body",
  "httparse",
@@ -1583,6 +1683,22 @@ dependencies = [
 ]
 
 [[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
 name = "hyper-util"
 version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2130,6 +2246,23 @@ dependencies = [
 ]
 
 [[package]]
+name = "native-tls"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
 name = "ndk"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2475,12 +2608,66 @@ dependencies = [
 ]
 
 [[package]]
+name = "openssl"
+version = "0.10.70"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
+dependencies = [
+ "bitflags 2.8.0",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.98",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
 name = "option-ext"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
 
 [[package]]
+name = "ordered-multimap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.14.5",
+]
+
+[[package]]
 name = "ordered-stream"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3073,18 +3260,22 @@ checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
 dependencies = [
  "base64 0.22.1",
  "bytes",
+ "encoding_rs",
  "futures-core",
  "futures-util",
+ "h2",
  "http",
  "http-body",
  "http-body-util",
  "hyper",
  "hyper-rustls",
+ "hyper-tls",
  "hyper-util",
  "ipnet",
  "js-sys",
  "log",
  "mime",
+ "native-tls",
  "once_cell",
  "percent-encoding",
  "pin-project-lite",
@@ -3096,7 +3287,9 @@ dependencies = [
  "serde_json",
  "serde_urlencoded",
  "sync_wrapper",
+ "system-configuration",
  "tokio",
+ "tokio-native-tls",
  "tokio-rustls",
  "tokio-util",
  "tower",
@@ -3107,7 +3300,7 @@ dependencies = [
  "wasm-streams",
  "web-sys",
  "webpki-roots",
- "windows-registry",
+ "windows-registry 0.2.0",
 ]
 
 [[package]]
@@ -3125,6 +3318,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "rust-ini"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+ "trim-in-place",
+]
+
+[[package]]
 name = "rustc-demangle"
 version = "0.1.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3223,6 +3427,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
 name = "schemars"
 version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3256,6 +3469,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.8.0",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
 name = "selectors"
 version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3510,7 +3746,7 @@ dependencies = [
  "bytemuck",
  "cfg_aliases",
  "core-graphics",
- "foreign-types",
+ "foreign-types 0.5.0",
  "js-sys",
  "log",
  "objc2",
@@ -3652,6 +3888,27 @@ dependencies = [
 ]
 
 [[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags 2.8.0",
+ "core-foundation 0.9.4",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
 name = "system-deps"
 version = "6.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3672,7 +3929,7 @@ checksum = "3731d04d4ac210cd5f344087733943b9bfb1a32654387dad4d1c70de21aee2c9"
 dependencies = [
  "bitflags 2.8.0",
  "cocoa",
- "core-foundation",
+ "core-foundation 0.10.0",
  "core-graphics",
  "crossbeam-channel",
  "dispatch",
@@ -3850,6 +4107,26 @@ dependencies = [
 ]
 
 [[package]]
+name = "tauri-plugin-deep-link"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35d51ffd286073414d26353bcfc9e83e3cd63f96fa7f7a912f92f2118e5de5a6"
+dependencies = [
+ "dunce",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "tauri-utils",
+ "thiserror 2.0.11",
+ "tracing",
+ "url",
+ "windows-registry 0.3.0",
+ "windows-result",
+]
+
+[[package]]
 name = "tauri-plugin-opener"
 version = "2.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3872,6 +4149,22 @@ dependencies = [
 ]
 
 [[package]]
+name = "tauri-plugin-single-instance"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47c387d4d96690131dc46d1d2827df5c222b896a2bfeb15a16267229a55c50b5"
+dependencies = [
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin-deep-link",
+ "thiserror 2.0.11",
+ "tracing",
+ "windows-sys 0.59.0",
+ "zbus",
+]
+
+[[package]]
 name = "tauri-runtime"
 version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4066,6 +4359,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
 name = "tinystr"
 version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4106,6 +4408,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
 name = "tokio-rustls"
 version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4278,6 +4590,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "trim-in-place"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
+
+[[package]]
 name = "try-lock"
 version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4418,6 +4736,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
 name = "version-compare"
 version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4755,7 +5079,7 @@ dependencies = [
  "windows-implement",
  "windows-interface",
  "windows-result",
- "windows-strings",
+ "windows-strings 0.1.0",
  "windows-targets 0.52.6",
 ]
 
@@ -4788,7 +5112,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
 dependencies = [
  "windows-result",
- "windows-strings",
+ "windows-strings 0.1.0",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-registry"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a"
+dependencies = [
+ "windows-result",
+ "windows-strings 0.2.0",
  "windows-targets 0.52.6",
 ]
 
@@ -4812,6 +5147,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "windows-strings"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
 name = "windows-sys"
 version = "0.45.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/app/Cargo.toml b/app/Cargo.toml
index 45b24b1..f11ae44 100644
--- a/app/Cargo.toml
+++ b/app/Cargo.toml
@@ -11,7 +11,12 @@ crate-type = ["staticlib", "cdylib", "rlib"]
 tauri-build = { version = "2", features = [] }
 
 [dependencies]
-tauri = { version = "2", features = ["config-toml"] }
-tauri-plugin-opener = "2"
+reqwest = { version = "0.12.12", features = ["json"] }
 serde = { version = "1", features = ["derive"] }
 serde_json = "1"
+tauri = { version = "2", features = ["config-toml"] }
+tauri-plugin-opener = "2"
+tauri-plugin-deep-link = "2"
+tauri-plugin-single-instance = {version = "2", features = ["deep-link"] }
+tokio = "1.43.0"
+url = "2.5.4"
diff --git a/app/Tauri.toml b/app/Tauri.toml
index ad2aa7f..7617202 100644
--- a/app/Tauri.toml
+++ b/app/Tauri.toml
@@ -17,3 +17,8 @@ height = 600
 
 [bundle]
 active = true
+
+[plugins.deep-link.desktop]
+schemes = [
+  "dev.tempest.foxfleet"
+]
diff --git a/app/src/lib.rs b/app/src/lib.rs
index d9ad08c..ae80c24 100644
--- a/app/src/lib.rs
+++ b/app/src/lib.rs
@@ -1,14 +1,194 @@
+mod state;
+use state::AppState;
+
+use tauri_plugin_deep_link::DeepLinkExt;
+use tauri_plugin_opener::OpenerExt;
+
+use std::collections::HashMap;
+use tauri::{Manager, State, AppHandle};
+use url::Host;
+use serde::Deserialize;
+use tokio::sync::Mutex;
+use tokio::sync::mpsc::channel;
+
+const OAUTH_CLIENT_NAME: &'static str = "foxfleet_test";
+
 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 pub fn run() {
     tauri::Builder::default()
+        .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
+            println!("{}, {argv:?}, {cwd}", app.package_info().name);
+        }))
+        .plugin(tauri_plugin_deep_link::init())
         .plugin(tauri_plugin_opener::init())
-        .invoke_handler(tauri::generate_handler![greet])
+        .setup(|app| {
+            app.manage(Mutex::new(state::AppState::default()));
+
+            #[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
+            app.deep_link().register_all()?;
+            let app_handle = app.handle().clone();
+            app.deep_link().on_open_url(move |event| {
+                if let Some(oauth_callback) = event.urls().iter().find(|url| {
+                    if let Some(Host::Domain(domain)) = url.host() {
+                        if domain == "oauth-response" {
+                            return true;
+                        }
+                    }
+                    false
+                }) {
+                    let mut query = oauth_callback.query_pairs();
+                    if let Some(code) = query.find(|(key, _value)| key == "code") {
+                        let app_handle = app_handle.clone();
+                        let code = code.1.to_string();
+                        tauri::async_runtime::spawn(async move {
+                            let app_state = app_handle.state::<Mutex<AppState>>();
+                            app_state.lock().await.accounts.iter_mut().for_each(|account| {
+                                // TODO: handle if there's multiple of these that match
+                                if let state::ApiCredential::Pending(sender) = &account.api_credential {
+                                    let sender = sender.clone();
+                                    let code = code.clone();
+                                    tauri::async_runtime::spawn(async move {
+                                        let _ = sender.send(state::AuthCode(code)).await;
+                                    });
+                                }
+                            });
+                        });
+                    } else {
+                        println!("No code in oauth callback");
+                        return
+                    }
+                }
+            });
+            Ok(())
+        })
+        .invoke_handler(tauri::generate_handler![start_account_auth, get_self])
         .run(tauri::generate_context!())
         .expect("Error starting")
 }
 
+#[derive(Deserialize)]
+struct RegistrationResponse {
+    id: String,
+    name: String,
+    client_id: String,
+    client_secret: String,
+}
+
+#[derive(Deserialize)]
+struct TokenResponse {
+    access_token: String,
+    created_at: u32,
+    scope: String,
+    token_type: String,
+}
+
 #[tauri::command]
-fn greet(name: &str) -> String {
-   format!("Hello, {}!", name)
+async fn start_account_auth(app_handle: AppHandle, state: State<'_, Mutex<AppState>>, instance_domain: &str) -> Result<(), ()> {
+    println!("Starting account auth");
+    let registration_endpoint = format!("https://{instance_domain}/api/v1/apps");
+    let token_endpoint = format!("https://{instance_domain}/oauth/token");
+    let client = reqwest::Client::builder().user_agent("Foxfleet v0.0.1").build().expect("Could not construct client");
+    println!("Registering client");
+    let registration_response : RegistrationResponse = client.post(registration_endpoint)
+        .json(&HashMap::from([
+            ("client_name", OAUTH_CLIENT_NAME),
+            ("redirect_uris", "dev.tempest.foxfleet://oauth-response"),
+            ("scopes", "read write"),
+        ])).send().await.expect("Could not send client registration")
+        .json().await.expect("Could not parse client registration response");
+
+    // Make channel for awaiting
+    let (sender, mut receiver) = channel::<state::AuthCode>(1);
+
+    println!("Saving registration");
+    { state.lock().await.accounts.push(state::Account {
+        server_domain: instance_domain.to_string(),
+        handle_domain: None,
+        client_credential: state::ClientCredential {
+            client_name: OAUTH_CLIENT_NAME.to_string(),
+            client_id: registration_response.client_id.clone(),
+            client_secret: Some(registration_response.client_secret.clone()),
+        },
+        api_credential: state::ApiCredential::Pending(sender),
+    }) }
+
+    // Open browser to auth page
+    println!("Opening authentication page");
+    let client_id = registration_response.client_id.clone();
+    let auth_page = format!("https://{instance_domain}/oauth/authorize?client_id={client_id}&redirect_uri=dev.tempest.foxfleet://oauth-response&response_type=code&scope=read+write");
+    let opener = app_handle.opener();
+    if let Err(_) = opener.open_url(auth_page, None::<&str>) {
+        println!("Could not open authentication page");
+        return Err(())
+    }
+
+
+    // Wait for resolution of the credential
+    let auth_code = receiver.recv().await;
+
+    if auth_code.is_none() {
+        return Err(())
+    }
+
+    let auth_code = auth_code.unwrap();
+    println!("Exchanging auth code for API token");
+
+    // Get long-lived credential
+    let token_response : TokenResponse = client.post(token_endpoint)
+        .json(&HashMap::from([
+            ("redirect_uri", "dev.tempest.foxfleet://oauth-response"),
+            ("client_id", registration_response.client_id.as_str()),
+            ("client_secret", registration_response.client_secret.as_str()),
+            ("grant_type", "authorization_code"),
+            ("code", auth_code.0.as_str()),
+        ])).send().await.expect("Could not get API token")
+        .json().await.expect("Could not parse client registration response");
+
+    println!("Successfully exchanged for credential");
+
+    // Save credential
+    { state.lock().await.accounts.iter_mut().for_each(|account| {
+        if account.server_domain == instance_domain {
+            account.api_credential = state::ApiCredential::Some {
+                token: token_response.access_token.clone(),
+                refresh: None,
+            }
+        }
+    }) };
+
+    println!("Saved credential");
+
+    Ok(())
 }
 
+#[tauri::command]
+async fn get_self(state: State<'_, Mutex<AppState>>) -> Result<String, String> {
+    let client = reqwest::Client::builder().user_agent("Foxfleet v0.0.1").build().expect("Could not construct client");
+
+    let accounts = { state.lock().await.accounts.clone() };
+    let account = accounts.iter().find(|account| {
+        if let state::ApiCredential::Some {token: _, refresh: _} = account.api_credential {
+            true
+        } else {
+            false
+        }
+    });
+
+    if let Some(account) = account {
+        if let state::ApiCredential::Some {token, refresh: _} = &account.api_credential {
+            if let Ok(result) = client.get("https://social.tempest.dev/api/v1/accounts/verify_credentials")
+                .bearer_auth(token)
+                .send().await {
+                    if let Ok(result) = result.text().await {
+                        return Ok(result)
+                    } else {
+                        return Err("Error decoding response".to_string());
+                    }
+                } else {
+                    return Err("Error fetching account".to_string());
+                }
+        }
+    }
+
+    return Err("No logged in account".to_string());
+}
diff --git a/app/src/state.rs b/app/src/state.rs
new file mode 100644
index 0000000..44e74ed
--- /dev/null
+++ b/app/src/state.rs
@@ -0,0 +1,46 @@
+use tokio::sync::mpsc::Sender;
+
+#[derive(Clone)]
+pub struct AppState {
+    pub preferences: (),
+    pub accounts: Vec<Account>,
+}
+
+impl AppState {
+    pub fn default() -> Self {
+        Self {
+            preferences: (),
+            accounts: Vec::new(),
+
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct Account {
+    pub server_domain: String,
+    pub handle_domain: Option<String>,
+    pub client_credential: ClientCredential,
+    pub api_credential: ApiCredential,
+}
+
+#[derive(Clone)]
+pub struct ClientCredential {
+    pub client_name: String,
+    pub client_id: String,
+    pub client_secret: Option<String>,
+}
+
+#[derive(Clone)]
+pub struct AuthCode (pub String);
+
+#[derive(Clone)]
+pub enum ApiCredential {
+    None,
+    Pending(Sender<AuthCode>),
+    Some {
+        token: String,
+        refresh: Option<String>
+    }
+}
+
diff --git a/ui/src/root.tsx b/ui/src/root.tsx
index 8db8960..bc87cd9 100644
--- a/ui/src/root.tsx
+++ b/ui/src/root.tsx
@@ -2,20 +2,28 @@ import { useState } from 'react';
 import { invoke } from '@tauri-apps/api/core';
 
 export default function Root() {
-  const [rustResult, setRustResult] = useState('')
+  const [signedIn, setSignedIn] = useState(false)
+  const [accountData, setAccountData] = useState('')
 
-  async function callRust() {
-    const result : string = await invoke('greet', {name: 'ashe'})
-    setRustResult(result)
+  async function signIn() {
+    await invoke('start_account_auth', {instanceDomain: 'social.tempest.dev'})
+    setSignedIn(true)
+  }
+
+  async function getSelf() {
+    let result = await invoke('get_self') as string
+    setAccountData(JSON.parse(result))
   }
 
   return (
     <>
-      <p>Now we have React</p>
-      {rustResult
-        ? <p>Result from rust: <pre><code>{rustResult}</code></pre></p>
-        : <button onClick={callRust}>Call rust</button>
-      }
+      {!signedIn ? (
+        <button onClick={signIn}>Sign in</button>
+      ) : (!accountData ? (
+        <button onClick={getSelf}>Retrieve account data</button>
+      ):(
+        <p>Result from rust: <pre style={{whiteSpace:'pre'}}>{JSON.stringify(accountData, null, 2)}</pre></p>
+      ))}
     </>
   )
 }