diff --git a/desktop/NxNativeEasy/Localization.swift b/desktop/NxNativeEasy/Localization.swift index 7732835..1db21fd 100644 --- a/desktop/NxNativeEasy/Localization.swift +++ b/desktop/NxNativeEasy/Localization.swift @@ -12,6 +12,16 @@ enum EasyLanguage: String, CaseIterable { struct EasyStrings { static let english: [String:String] = [ + "Verifica tu cuenta":"Verify your account", + "Introduce los 6 dígitos que Apple muestra en tu dispositivo de confianza o envía por SMS.":"Enter the 6 digits Apple shows on your trusted device or sends by text message.", + "Puedes pegar el código completo.":"You can paste the entire code.", + "Verificar y continuar":"Verify and continue", + "¿No recibiste el código?":"Did not receive a code?", + "Elige dónde quieres recibir uno nuevo.":"Choose where to receive a new one.", + "Mostrar en mis dispositivos Apple":"Show on my Apple devices", + "Enviar SMS al número terminado en":"Text the number ending in", + "Solicitar otro código":"Request another code", + "El código se envía a Apple y no se guarda.":"The code is sent to Apple and is not saved.", "Prepara la IPA y te guiamos para instalarla.":"Prepare the IPA, then follow the guided installation.", "Una app completa. Preparar y jugar en este Mac.":"One complete app. Build and play on this Mac.", "Conviene recompilar":"Rebuild recommended", diff --git a/desktop/NxNativeEasy/Sideloading.swift b/desktop/NxNativeEasy/Sideloading.swift index c5992c2..94b855f 100644 --- a/desktop/NxNativeEasy/Sideloading.swift +++ b/desktop/NxNativeEasy/Sideloading.swift @@ -458,7 +458,6 @@ struct SideloadView:View { @Environment(\.dismiss) private var dismiss @State private var installStep=0 @State private var password="" - @State private var code="" @State private var remember=true @State private var autoRefresh=true @State private var loginItem=SMAppService.mainApp.status == .enabled @@ -487,7 +486,7 @@ struct SideloadView:View { } if accountOnly || installStep==1 { Text(text("Una cuenta para todos tus dispositivos en este Mac. Sólo se necesita para firmar, instalar y renovar apps.")).foregroundStyle(.secondary) - if !model.authenticated { + if !model.authenticated && !model.codeRequired { LabeledContent(text("Cuenta Apple personal")){TextField("name@example.com",text:$model.email).textFieldStyle(.roundedBorder).disabled(model.busy)} LabeledContent(text("Contraseña de Apple")){SecureField(text("Contraseña de Apple"),text:$password).textFieldStyle(.roundedBorder).disabled(model.busy)} Toggle(text("Recordar sesión en el Llavero; no guardar la contraseña"),isOn:$remember).disabled(model.busy).onChange(of:remember){_,value in if !value {autoRefresh=false}} @@ -497,7 +496,7 @@ struct SideloadView:View { } if model.hasRememberedSession {Button(text("Usar sesión del Llavero")){model.restoreSession()}.disabled(model.busy)} Button(text("Iniciar sesión")){model.signIn(password:password,remember:remember);password=""}.buttonStyle(.borderedProminent).disabled(model.busy || model.email.isEmpty || password.isEmpty) - } else { + } else if model.authenticated { Label(text("Sesión iniciada"),systemImage:"person.crop.circle.badge.checkmark") Text(model.email).font(.caption).textSelection(.enabled) Picker(text("Tu equipo personal"),selection:$model.team) {Text(text("Selecciona tu equipo")).tag("");ForEach(model.teams){Text($0.name+" · "+$0.id).tag($0.id)}}.disabled(model.busy) @@ -510,13 +509,8 @@ struct SideloadView:View { Text(text("Apple no informa los cupos restantes en esta consulta. Mostramos los dispositivos registrados, sin inventar disponibilidad ni eliminar registros.")).font(.caption).foregroundStyle(.secondary) Button(text("Cerrar sesión")){model.signOut()}.disabled(model.busy) } - if model.codeRequired { - TextField(text("Código de verificación"),text:$code) - HStack {Button(text("Confirmar código")){model.submitCode(code);code=""};Button(text("Reenviar código")){model.resend()}} - Button(text("Enviar a mis dispositivos")){model.sendToDevices()} - ForEach(model.smsNumbers) {number in Button("SMS · …"+number.digits){model.sendSMS(number.id)}} - } - if !accountOnly {Button(text("Continuar")){installStep=2}.buttonStyle(.borderedProminent).disabled(model.busy || !model.authenticated || model.team.isEmpty)} + if model.codeRequired {AppleVerificationView(model:model,text:text)} + if !accountOnly && !model.codeRequired {Button(text("Continuar")){installStep=2}.buttonStyle(.borderedProminent).disabled(model.busy || !model.authenticated || model.team.isEmpty)} } if !accountOnly && installStep==2 { Label(text("Revisa antes de instalar"),systemImage:"checklist").font(.title2.bold()) @@ -537,8 +531,8 @@ struct SideloadView:View { } } if !accountOnly && installStep==3 && !model.busy {Button(text("Revisar e intentar de nuevo")){installStep=2}} - if !accountOnly {Text(text("Guarda y cierra el juego antes de instalar. Firma, copia y renovación nativas aún requieren validación completa con una cuenta y dispositivo reales.")).font(.caption).foregroundStyle(.secondary)} - InstallActivityView(model:model,text:text) + if !accountOnly && !model.codeRequired {Text(text("Guarda y cierra el juego antes de instalar. Firma, copia y renovación nativas aún requieren validación completa con una cuenta y dispositivo reales.")).font(.caption).foregroundStyle(.secondary)} + if !model.codeRequired {InstallActivityView(model:model,text:text)} if !model.busy {Text(text(model.phase)).font(.callout)} }.padding(24) } @@ -548,6 +542,58 @@ struct SideloadView:View { } +struct AppleVerificationView:View { + @ObservedObject var model:SideloadModel + let text:(String)->String + @State private var code="" + @FocusState private var focused:Bool + private var valid:Bool {code.count==6 && code.utf8.allSatisfy {48...57 ~= $0}} + private func confirm() {guard valid else {return};let submitted=code;code="";model.submitCode(submitted)} + var body:some View { + VStack(alignment:.leading,spacing:22) { + HStack(alignment:.top,spacing:14) { + Image(systemName:"lock.shield").font(.system(size:28,weight:.light)).foregroundStyle(Color(red:0.79,green:0.95,blue:0.37)).frame(width:48,height:48).background(.white.opacity(0.04),in:RoundedRectangle(cornerRadius:12)) + VStack(alignment:.leading,spacing:6) { + Text(text("Verifica tu cuenta")).font(.title2.bold()) + if !model.email.isEmpty {Text(model.email).font(.caption).foregroundStyle(.secondary)} + Text(text("Introduce los 6 dígitos que Apple muestra en tu dispositivo de confianza o envía por SMS.")).font(.callout).foregroundStyle(.secondary).fixedSize(horizontal:false,vertical:true) + } + } + VStack(alignment:.leading,spacing:10) { + Text(text("Código de verificación")).font(.callout.weight(.semibold)) + TextField("······",text:$code).textFieldStyle(.plain).font(.system(size:30,weight:.medium,design:.monospaced)).tracking(7) + .padding(.horizontal,18).padding(.vertical,15).frame(width:250) + .background(.black.opacity(0.18),in:RoundedRectangle(cornerRadius:11)) + .overlay(RoundedRectangle(cornerRadius:11).strokeBorder(focused ? Color(red:0.79,green:0.95,blue:0.37).opacity(0.65) : .white.opacity(0.12))) + .focused($focused).accessibilityLabel(text("Código de verificación")) + .onChange(of:code){_,value in code=String(value.utf8.filter {48...57 ~= $0}.prefix(6).map {Character(UnicodeScalar($0))})} + .onSubmit {confirm()} + Text(text("Puedes pegar el código completo.")).font(.caption).foregroundStyle(.secondary) + } + Button(action:confirm){Label(text("Verificar y continuar"),systemImage:"arrow.right").font(.callout.bold()).padding(.horizontal,18).frame(minHeight:44) + .foregroundStyle(valid ? Color.black : Color.secondary) + .background(valid ? Color(red:0.79,green:0.95,blue:0.37) : .white.opacity(0.06),in:RoundedRectangle(cornerRadius:10))} + .buttonStyle(.plain).disabled(!valid) + Divider().overlay(.white.opacity(0.06)) + DisclosureGroup(text("¿No recibiste el código?")) { + VStack(alignment:.leading,spacing:12) { + Text(text("Elige dónde quieres recibir uno nuevo.")).font(.caption).foregroundStyle(.secondary) + Button {code="";model.sendToDevices()} label:{Label(text("Mostrar en mis dispositivos Apple"),systemImage:"ipad.and.iphone")} + ForEach(model.smsNumbers) {number in + Button {code="";model.sendSMS(number.id)} label:{Label(text("Enviar SMS al número terminado en")+" "+number.digits,systemImage:"message")} + } + Button(text("Solicitar otro código")){code="";model.resend()}.buttonStyle(.link) + }.padding(.top,12) + }.font(.callout) + Text(text("El código se envía a Apple y no se guarda.")).font(.caption).foregroundStyle(.secondary) + }.padding(24).frame(maxWidth:.infinity,alignment:.leading) + .background(.white.opacity(0.035),in:RoundedRectangle(cornerRadius:18)) + .overlay(RoundedRectangle(cornerRadius:18).strokeBorder(.white.opacity(0.06))) + .onAppear {focused=true} + } +} + + struct SideloadLibraryView:View { @ObservedObject var model:SideloadModel let text:(String)->String diff --git a/desktop/release.json b/desktop/release.json index 5d54be6..cee2a3e 100644 --- a/desktop/release.json +++ b/desktop/release.json @@ -1,5 +1,5 @@ { - "version": "1.13.0", - "build": 20, + "version": "1.13.1", + "build": 21, "minimumMacOSMajor": 26 } diff --git a/docs/sideloading.md b/docs/sideloading.md index fb0b95b..5765e63 100644 --- a/docs/sideloading.md +++ b/docs/sideloading.md @@ -307,3 +307,15 @@ app. Support for the new game version still depends on the engine/profile. A lightweight IPA still needs the complete RomFS on the phone; it makes app signing/reinstallation smaller, not the game data optional. Full-IPA mode embeds those data and retransfers the larger app on renewal. + +## Apple verification presentation (1.13.1) + +An active verification challenge replaces the password form with one card: +account context, six-digit ASCII code input, paste/Enter support and one primary +Verify and continue action. It remains disabled until six digits are entered. +Alternative trusted-device/SMS delivery and resend live under Did not receive a +code, rather than competing with verification. The code is cleared on submission +or requesting another delivery and is never persisted or logged. No password, +provider/session, request transport or delivery behavior changed. Generic busy +activity and the installer Continue button do not compete with a pending code. +The update does not restart a running launcher or interrupt its in-memory login.