Open in Google Play Console deep-links the app: Play developer id per credential profile (accounts play-developer, desktop Accounts), shared playAppUrl builder for desktop, CLI and watch; developer chooser instead of the console root
This commit is contained in:
parent
7c769c18f4
commit
4a74989a95
33 changed files with 311 additions and 40 deletions
|
|
@ -31,6 +31,10 @@ pub struct Account {
|
|||
/// None only for an account of an earlier version whose keychain entry has not moved yet.
|
||||
#[serde(default)]
|
||||
pub profile: Option<String>,
|
||||
/// Play only: the developer account id (PLAY_DEVELOPER_ID in its credential profile, shared with the CLI), for
|
||||
/// Play Console deep links. Read from the profile on every list; never a secret.
|
||||
#[serde(default, rename = "developerId", skip_serializing_if = "Option::is_none")]
|
||||
pub developer_id: Option<String>,
|
||||
}
|
||||
|
||||
/// The keychain entry of one account (JSON).
|
||||
|
|
|
|||
|
|
@ -803,7 +803,11 @@ pub fn project_set_accounts(window: WebviewWindow, state: State<'_, AppState>, i
|
|||
pub fn accounts_list(window: WebviewWindow, state: State<'_, AppState>) -> Res<Vec<Account>> {
|
||||
guard(&window)?;
|
||||
sync_machine(&state)?;
|
||||
Ok(saved(&state)?.accounts.clone())
|
||||
let mut out = saved(&state)?.accounts.clone();
|
||||
for a in out.iter_mut().filter(|a| a.kind == accounts::AccountKind::Play) {
|
||||
a.developer_id = a.profile.as_deref().and_then(|p| state.profiles.play_developer_id(p));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn check_studio(state: &AppState, studio: &Option<String>) -> Res<()> {
|
||||
|
|
@ -834,7 +838,7 @@ fn add_account(state: &AppState, label: &str, secret: Secret, studio: Option<Str
|
|||
}
|
||||
let id = profiles::account_id(secret.kind(), &profile);
|
||||
let id = if st.accounts.iter().any(|a| a.id == id) { store::new_id()? } else { id };
|
||||
let acc = Account { id, label, kind: secret.kind(), created: store::now_iso(), studio, profile: Some(profile) };
|
||||
let acc = Account { id, label, kind: secret.kind(), created: store::now_iso(), studio, profile: Some(profile), developer_id: None };
|
||||
st.accounts.push(acc.clone());
|
||||
persist(state, &st)?;
|
||||
Ok(acc)
|
||||
|
|
@ -1135,6 +1139,23 @@ pub fn account_update(window: WebviewWindow, state: State<'_, AppState>, id: Str
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
/// Sets or clears the Play developer account id of a Play account (PLAY_DEVELOPER_ID in its credential profile, the
|
||||
/// same field `storebound accounts play-developer` writes): Play Console links then open each app's own page.
|
||||
#[tauri::command]
|
||||
pub fn account_set_play_developer(window: WebviewWindow, state: State<'_, AppState>, id: String, developer_id: Option<String>) -> Res<Account> {
|
||||
guard(&window)?;
|
||||
let st = saved(&state)?;
|
||||
let mut a = st.accounts.iter().find(|a| a.id == id).cloned().ok_or_else(|| Fail::host("unknown account"))?;
|
||||
if a.kind != accounts::AccountKind::Play {
|
||||
return Err(Fail::host("a Play account only"));
|
||||
}
|
||||
let profile = a.profile.clone().ok_or_else(|| Fail::host("this account has no credential profile yet"))?;
|
||||
let v = developer_id.as_deref().map(str::trim).filter(|v| !v.is_empty());
|
||||
state.profiles.set_play_developer_id(&profile, v).map_err(Fail::host)?;
|
||||
a.developer_id = state.profiles.play_developer_id(&profile);
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
/// Does the account's key work, and can it see apps (sidecar `account.check`). Not cached: it is a live answer.
|
||||
#[tauri::command]
|
||||
pub async fn account_check(window: WebviewWindow, app: AppHandle, state: State<'_, AppState>, id: String, job: Option<String>) -> Res<Value> {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ pub const COMMANDS: &[&str] = &[
|
|||
"studio_remove",
|
||||
"studio_logo",
|
||||
"account_update",
|
||||
"account_set_play_developer",
|
||||
"account_check",
|
||||
"store_apps_refresh",
|
||||
"store_apps_cached",
|
||||
|
|
|
|||
|
|
@ -344,6 +344,7 @@ pub fn run() {
|
|||
commands::studio_remove,
|
||||
commands::studio_logo,
|
||||
commands::account_update,
|
||||
commands::account_set_play_developer,
|
||||
commands::account_check,
|
||||
commands::store_apps_refresh,
|
||||
commands::store_apps_cached,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ pub fn slug(label: &str) -> String {
|
|||
|
||||
/// The CLI workspace's account id for a profile's store: sha256("<apple|play>:<profile>"), 16 hex (cli/lib/workspace.ts
|
||||
/// accountId), so the same account has the same id in `storebound.workspace.json` and in the app.
|
||||
/// A Play developer account id: 1-25 digits.
|
||||
pub fn valid_developer_id(v: &str) -> bool {
|
||||
!v.is_empty() && v.len() <= 25 && v.bytes().all(|b| b.is_ascii_digit())
|
||||
}
|
||||
|
||||
pub fn account_id(kind: AccountKind, profile: &str) -> String {
|
||||
let store = match kind {
|
||||
AccountKind::Apple => "apple",
|
||||
|
|
@ -139,6 +144,24 @@ impl ProfileStore {
|
|||
out
|
||||
}
|
||||
|
||||
/// The Play developer account id a profile keeps (PLAY_DEVELOPER_ID, digits), if any.
|
||||
pub fn play_developer_id(&self, name: &str) -> Option<String> {
|
||||
self.read(name).ok().and_then(|m| str_of(&m, "PLAY_DEVELOPER_ID").map(str::trim).filter(|v| valid_developer_id(v)).map(str::to_string))
|
||||
}
|
||||
|
||||
/// Sets or clears (None) PLAY_DEVELOPER_ID in an existing profile; every other field stays.
|
||||
pub fn set_play_developer_id(&self, name: &str, id: Option<&str>) -> Result<(), String> {
|
||||
let mut m = self.read(name)?;
|
||||
match id {
|
||||
Some(v) if valid_developer_id(v) => { m.insert("PLAY_DEVELOPER_ID".into(), Value::String(v.to_string())); }
|
||||
Some(_) => return Err("the Play developer id is the digits in play.google.com/console/u/0/developers/<id>".into()),
|
||||
None => { m.remove("PLAY_DEVELOPER_ID"); }
|
||||
}
|
||||
let text = serde_json::to_string_pretty(&Value::Object(m)).map_err(|e| e.to_string())? + "
|
||||
";
|
||||
write_private(&self.dir.join(format!("{name}.json")), text.as_bytes())
|
||||
}
|
||||
|
||||
/// The label the profile carries, if any (written by the app; the CLI ignores it).
|
||||
pub fn label(&self, name: &str) -> Option<String> {
|
||||
self.read(name).ok().and_then(|m| str_of(&m, "label").map(str::to_string))
|
||||
|
|
@ -367,7 +390,7 @@ pub fn sync(accounts: &mut Vec<Account>, store: &ProfileStore, legacy: &dyn Secr
|
|||
if accounts.iter().any(|a| a.id == id) {
|
||||
continue;
|
||||
}
|
||||
accounts.push(Account { id: id.clone(), label: store.label(&name).unwrap_or_else(|| name.clone()), kind, created: now.to_string(), studio: None, profile: Some(name.clone()) });
|
||||
accounts.push(Account { id: id.clone(), label: store.label(&name).unwrap_or_else(|| name.clone()), kind, created: now.to_string(), studio: None, profile: Some(name.clone()), developer_id: None });
|
||||
r.added.push(format!("{id} ({name})"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue