···11+// Placeholder for auth_store module
22+33+use atrium_api::types::string::Did;
44+use serde::{Deserialize, Serialize};
55+use yewdux::prelude::*;
66+77+// Yewdux Store for Authentication State
88+#[derive(Default, Clone, PartialEq, Serialize, Deserialize, Store)] // Added Serialize/Deserialize based on at_2048
99+pub struct AuthStore {
1010+ pub did: Option<Did>,
1111+}
+3
rust_demo_app/app_demo/src/components.rs
···11+// Placeholder for components module
22+33+// Module for shared UI components (currently empty)
+12-2
rust_demo_app/app_demo/src/main.rs
···33use log::info;
44use serde_json::Value;
55use std::rc::Rc;
66-use types_demo::CounterState;
76use wasm_bindgen_futures::spawn_local;
87use yew::prelude::*;
98use yew_hooks::use_effect_once;
···1312// Import the Store trait
1413use atrium_common::store::Store;
15141515+mod app_routes;
1616mod atrium_stores;
1717+mod auth_store;
1818+mod components;
1719mod idb;
1820mod oauth_client;
2121+mod pages;
19222023// Add pages module
2121-pub mod pages;
2424+// pub mod pages; // Remove this duplicate
22252326// Import page components
2427use crate::pages::callback::CallbackPage;
···3235use atrium_api::types::string::Did;
33363437const API_ROOT: &str = "";
3838+3939+// Define CounterState struct for local use in app_demo
4040+#[derive(Clone, Debug, PartialEq, Default, serde::Deserialize, serde::Serialize)]
4141+pub struct CounterState {
4242+ // Made pub for potential use in fetch_counter etc. if they move
4343+ pub value: i32,
4444+}
35453646// Define our application state
3747#[derive(Clone, Debug, PartialEq, Default)]
+193-6
rust_demo_app/app_demo/src/pages/home.rs
···11use crate::{
22 fetch_counter,
33- increment_counter,
33+ oauth_client::get_oauth_client,
44 reset_counter,
55 AppState, // Import reducer state
66 AuthStore,
77};
88+use atrium_api::agent::Agent; // Use Agent for XRPC calls
99+use atrium_api::com::atproto::repo::{
1010+ get_record::ParametersData as GetRecordParametersData,
1111+ put_record::InputData as PutRecordInputData,
1212+}; // Import specific data structs
1313+use atrium_api::types::string::{/* AtIdentifier, // Removed */ Datetime, Nsid, RecordKey};
1414+use atrium_api::types::{DataModel, /* Object, // Removed */ Unknown};
1515+use ipld_core::serde::to_ipld;
1616+use serde_json; // Added for record serialization
1717+use std::convert::TryFrom; // Added for try_from conversions
1818+use types_demo::dev::alternatebuild::rust_demo::count::{
1919+ /* Record as CountRecord, // Removed */ RecordData as CountRecordData,
2020+};
821use yew::prelude::*;
99-use yewdux::prelude::*;
2222+use yewdux::prelude::*; // Added for serde_ipld_dagcbor::to_ipld
10231124#[function_component(HomePage)]
1225pub fn home_page() -> Html {
···1427 let state = use_reducer(AppState::default);
1528 let (auth_store, _) = use_store::<AuthStore>(); // Get auth state to check if logged in
16293030+ // ATProto specific state
3131+ let has_counted_record = use_state(|| false);
3232+ let is_checking_record = use_state(|| true);
3333+ let increment_button_disabled = use_state(|| true);
3434+ let increment_button_text = use_state(|| "Increment".to_string());
3535+3636+ // Effect to check for existing ATProto record
3737+ {
3838+ let auth_store_for_effect = auth_store.clone();
3939+ let has_counted_record = has_counted_record.clone();
4040+ let is_checking_record = is_checking_record.clone();
4141+ let increment_button_disabled = increment_button_disabled.clone();
4242+ let increment_button_text = increment_button_text.clone();
4343+4444+ use_effect_with(auth_store_for_effect.did.clone(), move |did_option| {
4545+ let did_option = did_option.clone();
4646+ let has_counted_record = has_counted_record.clone();
4747+ let is_checking_record = is_checking_record.clone();
4848+ let increment_button_disabled = increment_button_disabled.clone();
4949+ let increment_button_text = increment_button_text.clone();
5050+5151+ is_checking_record.set(true);
5252+5353+ if let Some(did) = did_option.clone() {
5454+ wasm_bindgen_futures::spawn_local(async move {
5555+ let oauth_client = get_oauth_client().await;
5656+ match oauth_client.restore(&did).await {
5757+ Ok(session) => {
5858+ let agent = Agent::new(session);
5959+ let collection =
6060+ Nsid::new(String::from("dev.alternatebuild.rust_demo.count"))
6161+ .expect("Static NSID failed to parse");
6262+ let rkey = RecordKey::new(String::from("self"))
6363+ .expect("Static RecordKey failed to parse");
6464+ let params = GetRecordParametersData {
6565+ repo: did.into(),
6666+ collection,
6767+ rkey,
6868+ cid: None,
6969+ }
7070+ .into();
7171+7272+ match agent.api.com.atproto.repo.get_record(params).await {
7373+ Ok(_) => {
7474+ has_counted_record.set(true);
7575+ increment_button_text.set("Already Counted".to_string());
7676+ increment_button_disabled.set(true);
7777+ }
7878+ Err(e) => {
7979+ // Check if error is RecordNotFound
8080+ // (Need to inspect the specific error type `e` from `get_record` result)
8181+ // For now, assume any error means no record or inaccessible
8282+ has_counted_record.set(false);
8383+ increment_button_text.set("Increment".to_string());
8484+ increment_button_disabled.set(false);
8585+ log::debug!("get_record failed (likely not found): {:?}", e);
8686+ }
8787+ }
8888+ }
8989+ Err(_e) => {
9090+ has_counted_record.set(false);
9191+ increment_button_text.set("Session error. Try login.".to_string());
9292+ increment_button_disabled.set(true);
9393+ }
9494+ }
9595+ is_checking_record.set(false);
9696+ });
9797+ } else {
9898+ has_counted_record.set(false);
9999+ increment_button_disabled.set(true);
100100+ increment_button_text.set("Login to Count".to_string());
101101+ is_checking_record.set(false);
102102+ }
103103+ || ()
104104+ });
105105+ }
106106+17107 // Effect to load counter on mount
18108 {
19109 let state = state.clone();
···24114 }
2511526116 let on_increment = {
2727- let state = state.clone();
117117+ let auth_store_for_increment = auth_store.clone();
118118+ let has_counted_record_for_increment = has_counted_record.clone();
119119+ let increment_button_disabled_for_increment = increment_button_disabled.clone();
120120+ let increment_button_text_for_increment = increment_button_text.clone();
121121+28122 Callback::from(move |_| {
2929- increment_counter(state.clone());
123123+ let auth_store = auth_store_for_increment.clone();
124124+ let has_counted_record = has_counted_record_for_increment.clone();
125125+ let increment_button_disabled = increment_button_disabled_for_increment.clone();
126126+ let increment_button_text = increment_button_text_for_increment.clone();
127127+128128+ if !*has_counted_record && auth_store.did.is_some() {
129129+ let did_for_put = auth_store.did.clone().unwrap();
130130+ wasm_bindgen_futures::spawn_local(async move {
131131+ let oauth_client = get_oauth_client().await;
132132+ match oauth_client.restore(&did_for_put).await {
133133+ Ok(session) => {
134134+ let agent = Agent::new(session);
135135+ let record_body = CountRecordData {
136136+ created_at: Datetime::now(),
137137+ };
138138+ // Serialize record_body to Unknown via Ipld & DataModel
139139+ let record_unknown = match serde_json::to_value(record_body) {
140140+ Ok(value) => match to_ipld(&value) {
141141+ Ok(ipld) => match DataModel::try_from(ipld) {
142142+ Ok(data_model) => Unknown::Other(data_model), // Construct Unknown
143143+ Err(e) => {
144144+ log::error!(
145145+ "Failed to convert Ipld to DataModel: {:?}",
146146+ e
147147+ );
148148+ increment_button_text
149149+ .set("DataModel Error".to_string());
150150+ increment_button_disabled.set(true);
151151+ return;
152152+ }
153153+ },
154154+ Err(e) => {
155155+ log::error!(
156156+ "Failed to convert serde_json::Value to Ipld: {:?}",
157157+ e
158158+ );
159159+ increment_button_text
160160+ .set("Ipld Conversion Error".to_string());
161161+ increment_button_disabled.set(true);
162162+ return;
163163+ }
164164+ },
165165+ Err(e) => {
166166+ log::error!("Failed to serialize record body: {:?}", e);
167167+ increment_button_text.set("Serialization Error".to_string());
168168+ increment_button_disabled.set(true);
169169+ return;
170170+ }
171171+ };
172172+173173+ let collection =
174174+ Nsid::new(String::from("dev.alternatebuild.rust_demo.count"))
175175+ .expect("Static NSID failed to parse");
176176+ let rkey = RecordKey::new(String::from("self"))
177177+ .expect("Static RecordKey failed to parse");
178178+ let input = PutRecordInputData {
179179+ repo: did_for_put.into(),
180180+ collection,
181181+ rkey,
182182+ validate: Some(true),
183183+ record: record_unknown,
184184+ swap_commit: None,
185185+ swap_record: None,
186186+ }
187187+ .into();
188188+189189+ match agent.api.com.atproto.repo.put_record(input).await {
190190+ // Pass input struct
191191+ Ok(_) => {
192192+ has_counted_record.set(true);
193193+ increment_button_disabled.set(true);
194194+ increment_button_text.set("Counted!".to_string());
195195+ }
196196+ Err(_e) => {
197197+ increment_button_text
198198+ .set("Failed to count. Retry?".to_string());
199199+ increment_button_disabled.set(false);
200200+ log::error!("put_record failed: {:?}", _e);
201201+ }
202202+ }
203203+ }
204204+ Err(_e) => {
205205+ increment_button_text.set("Session error. Try login.".to_string());
206206+ increment_button_disabled.set(true);
207207+ }
208208+ }
209209+ });
210210+ }
30211 })
31212 };
32213···55236 } else if let Some(counter_data) = &state.counter {
56237 <p class="text-3xl font-bold">{ counter_data.value }</p>
57238 <div class="mt-4 space-x-2">
5858- <button onclick={on_increment} class="btn btn-primary">{ "Increment" }</button>
5959- <button onclick={on_reset} class="btn btn-secondary">{ "Reset" }</button>
239239+ if *is_checking_record {
240240+ <p>{ "Checking your ATProto record..." }</p>
241241+ } else {
242242+ <button onclick={on_increment} disabled={*increment_button_disabled} class="btn btn-primary">
243243+ { &*increment_button_text }
244244+ </button>
245245+ }
246246+ <button onclick={on_reset} class="btn btn-secondary">{ "Reset Shared Counter" }</button>
60247 </div>
61248 } else {
62249 <p>{ "Counter state not available." }</p>
+27-3
rust_demo_app/justfile
···99# Run both API and frontend servers
1010dev:
1111 #!/usr/bin/env sh
1212- trap 'kill $(jobs -p)' EXIT
1313- just api &
1414- just app 1212+ # Start API in background and get its PID
1313+ just api &
1414+ API_PID=$!
1515+ echo "API server started in background (PID: $API_PID)"
1616+1717+ # Define cleanup function
1818+ cleanup() {
1919+ echo "Shutting down API server (PID: $API_PID)..."
2020+ # Use kill -0 to check if process exists before trying to kill
2121+ if kill -0 $API_PID 2>/dev/null; then
2222+ kill $API_PID
2323+ wait $API_PID 2>/dev/null # Wait briefly for it to exit
2424+ else
2525+ echo "API server (PID: $API_PID) not found."
2626+ fi
2727+ # Exit script cleanly
2828+ exit 0
2929+ }
3030+3131+ # Trap signals to run cleanup
3232+ trap cleanup INT TERM EXIT
3333+3434+ # Start frontend in foreground (script will block here until app stops)
3535+ just app
3636+3737+ # Fallback cleanup if app exits normally (trap EXIT should handle Ctrl+C)
3838+ cleanup
+1
rust_demo_app/types_demo/Cargo.toml
···66[dependencies]
77schemars = { version = "0.8.22", features = ["derive"] }
88serde = { version = "1.0", features = ["derive"] }
99+atrium-api = { git = "https://github.com/atrium-rs/atrium" }
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `dev` namespace.
33+pub mod alternatebuild;
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `dev.alternatebuild` namespace.
33+44+pub mod rust_demo;
···11+// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
22+//!Definitions for the `dev.alternatebuild.rust_demo` namespace.
33+pub mod count;
44+#[derive(Debug)]
55+pub struct Count;
66+impl atrium_api::types::Collection for Count {
77+ const NSID: &'static str = "dev.alternatebuild.rust_demo.count";
88+ type Record = count::Record;
99+}