diff --git a/frontend/src/apple-signing.ts b/frontend/src/apple-signing.ts
index 6e2c0b3..a081298 100644
--- a/frontend/src/apple-signing.ts
+++ b/frontend/src/apple-signing.ts
@@ -122,21 +122,16 @@ export async function loginAppleDeveloperAccount(
password,
request.anisetteData,
(submitCode) => {
- if (request.onTwoFactorRequired) {
- request.onTwoFactorRequired((code) => {
- const normalized = code.trim()
- if (normalized.length === 0) {
- throw new Error("2FA code is required")
- }
- submitCode(normalized)
- })
- return
+ if (!request.onTwoFactorRequired) {
+ throw new Error("2FA required but no in-page handler provided")
}
- const code = window.prompt("Apple 2FA code")
- if (!code || code.trim().length === 0) {
- throw new Error("2FA code is required")
- }
- submitCode(code.trim())
+ request.onTwoFactorRequired((code) => {
+ const normalized = code.trim()
+ if (normalized.length === 0) {
+ throw new Error("2FA code is required")
+ }
+ submitCode(normalized)
+ })
},
)
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index d337760..daf5ff6 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -384,6 +384,20 @@ app.innerHTML = `
+
+
+ Two-Factor Authentication
+ Enter the verification code from your trusted device.
+
+
+
+
+
+
+
+
+
+
Confirm Trust on Device
@@ -430,6 +444,11 @@ const statusLine = mustGetElement("status-line")
const installProgressTextView = mustGetElement("install-progress-text")
const installProgressBarView = mustGetElement("install-progress-bar")
const logView = mustGetElement("log")
+const twoFactorModal = mustGetElement("two-factor-modal")
+const twoFactorCodeInput = mustGetInput("two-factor-code")
+const twoFactorErrorView = mustGetElement("two-factor-error")
+const twoFactorSubmitButton = mustGetButton("two-factor-submit")
+const twoFactorCancelButton = mustGetButton("two-factor-cancel")
const trustModal = mustGetElement("trust-modal")
const trustModalCloseButton = mustGetButton("trust-modal-close")
@@ -454,7 +473,10 @@ let busyInstall = false
let installProgressPercent = 0
let installProgressStatus = "idle"
let waitingForTrustConfirmation = false
+let waitingForTwoFactorCode = false
+let twoFactorSubmitHandler: ((code: string) => void) | null = null
let trustModalVisible = false
+let twoFactorModalVisible = false
let currentPage: AppPage = resolvePageFromHash(window.location.hash)
let selectedTargetUdid = loadText(SELECTED_DEVICE_UDID_STORAGE_KEY) ?? ""
@@ -484,6 +506,58 @@ const notifyPairingTrustPending = (): void => {
refreshUi()
}
+const requestTwoFactorCode = (submitCode: (code: string) => void): void => {
+ waitingForTwoFactorCode = true
+ twoFactorSubmitHandler = submitCode
+ twoFactorCodeInput.value = ""
+ twoFactorErrorView.textContent = ""
+ twoFactorModalVisible = true
+ addLog("login: 2FA required, waiting for code")
+ refreshUi()
+ window.setTimeout(() => {
+ twoFactorCodeInput.focus()
+ twoFactorCodeInput.select()
+ }, 0)
+}
+
+const submitTwoFactorCode = (): void => {
+ if (!twoFactorSubmitHandler) {
+ return
+ }
+ const code = twoFactorCodeInput.value.trim()
+ if (code.length === 0) {
+ twoFactorErrorView.textContent = "Please enter verification code."
+ return
+ }
+
+ const submit = twoFactorSubmitHandler
+ twoFactorSubmitHandler = null
+ waitingForTwoFactorCode = false
+ twoFactorModalVisible = false
+ twoFactorErrorView.textContent = ""
+ refreshUi()
+ submit(code)
+}
+
+const cancelTwoFactorCode = (): void => {
+ if (!twoFactorSubmitHandler) {
+ twoFactorModalVisible = false
+ waitingForTwoFactorCode = false
+ refreshUi()
+ return
+ }
+
+ const submit = twoFactorSubmitHandler
+ twoFactorSubmitHandler = null
+ waitingForTwoFactorCode = false
+ twoFactorModalVisible = false
+ twoFactorCodeInput.value = ""
+ twoFactorErrorView.textContent = ""
+ refreshUi()
+ addLog("login: 2FA canceled")
+ submit("__CANCELLED__")
+}
+
const closeTrustModal = (): void => {
trustModalVisible = false
refreshUi()
@@ -655,6 +729,9 @@ const loginAndSignFlow = async (): Promise => {
anisetteData: anisette,
credentials: { appleId, password },
onLog: addLog,
+ onTwoFactorRequired: (submitCode) => {
+ requestTwoFactorCode(submitCode)
+ },
})
loginContext = await refreshAppleDeveloperContext(context, addLog)
@@ -672,6 +749,10 @@ const loginAndSignFlow = async (): Promise => {
addLog("login: done. continue on sign/install page")
navigateToPage("sign")
} finally {
+ waitingForTwoFactorCode = false
+ twoFactorSubmitHandler = null
+ twoFactorModalVisible = false
+ twoFactorErrorView.textContent = ""
busyLoginSign = false
refreshUi()
}
@@ -850,7 +931,9 @@ const refreshUi = (): void => {
trustModal.classList.toggle("open", trustModalVisible)
trustModal.setAttribute("aria-hidden", trustModalVisible ? "false" : "true")
- document.body.classList.toggle("modal-open", trustModalVisible)
+ twoFactorModal.classList.toggle("open", twoFactorModalVisible)
+ twoFactorModal.setAttribute("aria-hidden", twoFactorModalVisible ? "false" : "true")
+ document.body.classList.toggle("modal-open", trustModalVisible || twoFactorModalVisible)
const currentSourceKey =
selectedIpaFile && selectedTargetUdid
@@ -861,7 +944,7 @@ const refreshUi = (): void => {
openSigningPageButton.disabled = busyLoginSign
pairDeviceButton.disabled = busyPairing || busySign || busyInstall || !isSupported
loginSignButton.disabled =
- busyLoginSign || appleIdInput.value.trim().length === 0 || applePasswordInput.value.length === 0
+ busyLoginSign || waitingForTwoFactorCode || appleIdInput.value.trim().length === 0 || applePasswordInput.value.length === 0
signButton.disabled =
busyPairing || busyLoginSign || busySign || busyInstall || !selectedIpaFile || !loginContext || selectedTargetUdid.length === 0
installButton.disabled =
@@ -1221,6 +1304,32 @@ dropArea.addEventListener("drop", (event) => {
refreshUi()
})
+twoFactorSubmitButton.addEventListener("click", () => {
+ submitTwoFactorCode()
+})
+
+twoFactorCancelButton.addEventListener("click", () => {
+ cancelTwoFactorCode()
+})
+
+twoFactorCodeInput.addEventListener("keydown", (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault()
+ submitTwoFactorCode()
+ return
+ }
+ if (event.key === "Escape") {
+ event.preventDefault()
+ cancelTwoFactorCode()
+ }
+})
+
+twoFactorCodeInput.addEventListener("input", () => {
+ if (twoFactorErrorView.textContent) {
+ twoFactorErrorView.textContent = ""
+ }
+})
+
trustModalCloseButton.addEventListener("click", () => {
closeTrustModal()
})