diff --git a/src-tauri/src/account.rs b/src-tauri/src/account.rs index 804b035..2e06ea0 100644 --- a/src-tauri/src/account.rs +++ b/src-tauri/src/account.rs @@ -5,13 +5,17 @@ use isideload::{ app_ids::{AppIdsApi, ListAppIdsResponse}, certificates::{CertificatesApi, DevelopmentCertificate}, developer_session::DeveloperSession, + teams::DeveloperTeam, + }, + sideload::{ + SideloaderBuilder, TeamSelection, builder::MaxCertsBehavior, sideloader::Sideloader, }, - sideload::{SideloaderBuilder, builder::MaxCertsBehavior, sideloader::Sideloader}, }; use keyring::Entry; +use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::time::Duration; +use std::{sync::Mutex, time::Duration}; use tauri::{AppHandle, Emitter, Listener, State, Window}; use tauri_plugin_store::StoreExt; use tracing::debug; @@ -22,6 +26,48 @@ use crate::{ sideload::{SideloaderGuard, SideloaderMutex}, }; +static TEAM_SELECTION_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)); + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DeveloperTeamInfo { + name: Option, + team_id: String, + r#type: Option, + status: Option, +} + +impl From<&DeveloperTeam> for DeveloperTeamInfo { + fn from(team: &DeveloperTeam) -> Self { + Self { + name: team.name.clone(), + team_id: team.team_id.clone(), + r#type: team.r#type.clone(), + status: team.status.clone(), + } + } +} + +fn prompt_for_team(teams: &Vec) -> Option { + let window = TEAM_SELECTION_WINDOW.lock().ok()?.as_ref().cloned()?; + let team_infos = teams + .iter() + .map(DeveloperTeamInfo::from) + .collect::>(); + + window.emit("team-selection-required", team_infos).ok()?; + + let (tx, rx) = std::sync::mpsc::channel::>(); + let handler_id = window.listen("team-selection-response", move |event| { + let selection = serde_json::from_str::>(event.payload()).unwrap_or(None); + let _ = tx.send(selection); + }); + + let result = rx.recv_timeout(Duration::from_secs(300)); + window.unlisten(handler_id); + result.unwrap_or(None) +} + #[tauri::command] pub async fn login_new( handle: AppHandle, @@ -122,6 +168,20 @@ pub fn logged_in_as(sideloader_state: State<'_, SideloaderMutex>) -> Option, +) -> Result, AppError> { + let mut sideloader = match SideloaderGuard::take(&sideloader_state) { + Ok(sideloader) => sideloader, + Err(AppError::NotLoggedIn) => return Ok(None), + Err(error) => return Err(error), + }; + let team = sideloader.get_mut().get_team().await?; + + Ok(Some(DeveloperTeamInfo::from(&team))) +} + #[tauri::command] pub fn invalidate_account(sideloader_state: State<'_, SideloaderMutex>) { let mut sideloader_guard = sideloader_state.lock().unwrap(); @@ -236,14 +296,23 @@ async fn login( } }; - // TODO: Team Selection - - let sideloader = SideloaderBuilder::new(dev_session, email.to_lowercase()) + let mut sideloader = SideloaderBuilder::new(dev_session, email.to_lowercase()) .machine_name("iloader".into()) .storage(create_sideloading_storage(app)?) + .team_selection(TeamSelection::PromptOnce(prompt_for_team)) .max_certs_behavior(MaxCertsBehavior::Prompt(Box::new(max_certs_callback))) .build(); + *TEAM_SELECTION_WINDOW + .lock() + .map_err(|_| AppError::Misc("Failed to prepare team selection".into()))? = + Some(window.clone()); + let team_result = sideloader.get_team().await; + *TEAM_SELECTION_WINDOW + .lock() + .map_err(|_| AppError::Misc("Failed to clean up team selection".into()))? = None; + team_result?; + debug!("Built sideloader"); Ok(sideloader) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b217fe9..8e97d4d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -15,7 +15,8 @@ mod operation; use crate::{ account::{ delete_account, delete_app_id, get_certificates, invalidate_account, list_app_ids, - logged_in_as, login_new, login_stored, reset_anisette_state, revoke_certificate, + logged_in_as, logged_in_team, login_new, login_stored, reset_anisette_state, + revoke_certificate, }, device::{ DeviceInfoMutex, PairingCancelToken, cancel_pairing, list_devices, set_selected_device, @@ -107,6 +108,7 @@ pub fn run() { login_new, invalidate_account, logged_in_as, + logged_in_team, login_stored, delete_account, list_devices, diff --git a/src/AppleID.css b/src/AppleID.css index dcc2f19..7edd4c2 100644 --- a/src/AppleID.css +++ b/src/AppleID.css @@ -61,6 +61,28 @@ color: #fff; } +.logged-team { + display: flex; + flex-direction: column; + gap: 0.15rem; + margin-top: 0.45rem; + padding-top: 0.55rem; + border-top: 1px solid rgba(255, 255, 255, 0.14); +} + +.logged-team-label { + color: rgba(255, 255, 255, 0.6); + font-size: 0.8rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.logged-team-value { + color: #fff; + font-size: 0.98rem; + font-weight: 600; +} + .credentials h3 { margin-bottom: 0.25em; margin-top: 1.5em; @@ -195,3 +217,73 @@ .credentials-warning { margin: 0; } + +.team-header { + margin-top: 0; +} + +.team-description { + margin-top: 0; + color: var(--text-muted); +} + +.team-list { + display: flex; + flex-direction: column; + gap: 0.6rem; + max-height: min(50vh, 420px); + overflow-y: auto; +} + +.team-item { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.8rem; + border: 1px solid var(--glass-border); + border-radius: 10px; + background: var(--glass-surface); + cursor: pointer; + transition: + border-color 0.2s ease, + background 0.2s ease; +} + +.team-item.selected { + border-color: var(--accent-secondary); + background: rgba(70, 130, 255, 0.14); +} + +.team-item input { + margin-top: 0.25rem; + accent-color: var(--accent-secondary); +} + +.team-details, +.team-metadata { + display: flex; + flex-direction: column; +} + +.team-details { + min-width: 0; + gap: 0.3rem; +} + +.team-name { + color: #fff; + font-weight: 600; +} + +.team-metadata { + gap: 0.15rem; + color: var(--text-muted); + font-size: 0.85rem; + overflow-wrap: anywhere; +} + +.team-buttons { + display: flex; + gap: 0.75rem; + margin-top: 1.25rem; +} diff --git a/src/AppleID.tsx b/src/AppleID.tsx index c89caa6..99ba043 100644 --- a/src/AppleID.tsx +++ b/src/AppleID.tsx @@ -12,6 +12,13 @@ import { useTranslation } from "react-i18next"; const storePromise = load("data.json"); +type DeveloperTeamInfo = { + name: string | null; + teamId: string; + type: string | null; + status: string | null; +}; + export const AppleID = ({ loggedInAs, setLoggedInAs, @@ -37,12 +44,19 @@ export const AppleID = ({ const [certs, setCerts] = useState(null); const [selectedSerials, setSelectedSerials] = useState([]); const [chooseCertsOpen, setChooseCertsOpen] = useState(false); + const [teams, setTeams] = useState(null); + const [selectedTeamId, setSelectedTeamId] = useState(""); + const [activeTeam, setActiveTeam] = useState(null); const { err } = useError(); useEffect(() => { let getLoggedInAs = async () => { let account = await invoke("logged_in_as"); setLoggedInAs(account); + let team = account + ? await invoke("logged_in_team") + : null; + setActiveTeam(team); }; let getStoredIds = async () => { const store = await storePromise; @@ -97,6 +111,28 @@ export const AppleID = ({ }; }, []); + const teamListenerAdded = useRef(false); + const teamUnlisten = useRef<() => void>(() => {}); + + useEffect(() => { + if (!teamListenerAdded.current) { + (async () => { + const unlistenFn = await listen( + "team-selection-required", + (event) => { + setSelectedTeamId(""); + setTeams(event.payload); + }, + ); + teamUnlisten.current = unlistenFn; + })(); + teamListenerAdded.current = true; + } + return () => { + teamUnlisten.current(); + }; + }, []); + return ( <>

{t("apple_id.title")}

@@ -106,6 +142,16 @@ export const AppleID = ({
{t("apple_id.logged_in_as")} {loggedInAs} + {activeTeam && ( + + + {t("apple_id.developer_team")} + + + {activeTeam.name ?? activeTeam.teamId} + + + )}
+ +

{t("apple_id.team_selection_title")}

+

+ {t("apple_id.team_selection_prompt")} +

+
+ {teams?.map((team) => ( + + ))} +
+
+ + +
+

{t("apple_id.two_factor_title")}

{t("apple_id.two_factor_prompt")}

diff --git a/src/locales/en.json b/src/locales/en.json index 17d70e0..1e948c0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -49,6 +49,7 @@ "apple_id": { "title": "Apple ID", "logged_in_as": "Logged in as", + "developer_team": "Developer Team", "sign_out": "Sign Out", "signing_out": "Signing Out...", "sign_out_failed": "Sign out failed", @@ -79,6 +80,12 @@ "hide_certificate_list": "Hide certificate list", "choose_what_to_revoke": "Choose what to revoke", "continue": "Continue", + "team_selection_title": "Choose a Developer Team", + "team_selection_prompt": "This account belongs to multiple developer teams. Choose the team to use for signing.", + "unnamed_team": "Unnamed Team", + "team_id": "Team ID", + "team_type": "Type", + "team_status": "Status", "no_keyring_available": "Unable to save credentials on this device." }, "device": { diff --git a/src/locales/zh_cn.json b/src/locales/zh_cn.json index 66ec27f..afe3f4c 100644 --- a/src/locales/zh_cn.json +++ b/src/locales/zh_cn.json @@ -49,6 +49,7 @@ "apple_id": { "title": "Apple ID", "logged_in_as": "已登录为", + "developer_team": "开发者团队", "sign_out": "登出", "signing_out": "正在登出...", "sign_out_failed": "登出失败", @@ -78,7 +79,13 @@ "max_certs_desc": "iloader 将撤销您现有的证书并生成一个新的。", "hide_certificate_list": "隐藏证书列表", "choose_what_to_revoke": "选择要撤销的内容", - "continue": "继续" + "continue": "继续", + "team_selection_title": "选择开发者团队", + "team_selection_prompt": "此账户属于多个开发者团队。请选择用于签名的团队。", + "unnamed_team": "未命名团队", + "team_id": "团队 ID", + "team_type": "类型", + "team_status": "状态" }, "device": { "title": "iDevice", diff --git a/src/locales/zh_hk.json b/src/locales/zh_hk.json index 893dc17..5ea8075 100644 --- a/src/locales/zh_hk.json +++ b/src/locales/zh_hk.json @@ -49,6 +49,7 @@ "apple_id": { "title": "Apple ID", "logged_in_as": "登入緊嘅帳戶", + "developer_team": "開發者團隊", "sign_out": "登出", "signing_out": "登出緊...", "sign_out_failed": "登出失敗", @@ -78,7 +79,13 @@ "max_certs_desc": "iloader 會撤銷你現有嘅憑證,再產生一個新嘅。", "hide_certificate_list": "收埋憑證列表", "choose_what_to_revoke": "揀要撤銷嘅嘢", - "continue": "繼續" + "continue": "繼續", + "team_selection_title": "揀開發者團隊", + "team_selection_prompt": "呢個帳戶屬於多個開發者團隊。請揀用嚟簽署嘅團隊。", + "unnamed_team": "未命名團隊", + "team_id": "團隊 ID", + "team_type": "類型", + "team_status": "狀態" }, "device": { "title": "iDevice", diff --git a/src/locales/zh_tw.json b/src/locales/zh_tw.json index 1f2376a..a51ea5e 100644 --- a/src/locales/zh_tw.json +++ b/src/locales/zh_tw.json @@ -49,6 +49,7 @@ "apple_id": { "title": "Apple ID", "logged_in_as": "已登入為", + "developer_team": "開發者團隊", "sign_out": "登出", "signing_out": "正在登出...", "sign_out_failed": "登出失敗", @@ -78,7 +79,13 @@ "max_certs_desc": "iloader 將撤銷您現有的憑證並產生一個新的。", "hide_certificate_list": "隱藏憑證列表", "choose_what_to_revoke": "選擇要撤銷的內容", - "continue": "繼續" + "continue": "繼續", + "team_selection_title": "選擇開發者團隊", + "team_selection_prompt": "此帳戶屬於多個開發者團隊。請選擇用於簽署的團隊。", + "unnamed_team": "未命名團隊", + "team_id": "團隊 ID", + "team_type": "類型", + "team_status": "狀態" }, "device": { "title": "iDevice",