Aggiunge gestione account con cambio password su web e app.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -55,6 +55,8 @@
|
||||
D7434175C1E849D7A8308896 /* MatchScreenScaffold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchScreenScaffold.swift; path = MatchLiveTv/UI/Components/MatchScreenScaffold.swift; sourceTree = "<group>"; };
|
||||
3DE0F41386A741B2A6FC538B /* MatchSecondaryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchSecondaryButton.swift; path = MatchLiveTv/UI/Components/MatchSecondaryButton.swift; sourceTree = "<group>"; };
|
||||
1F6A499FE7D94EF498DA9A84 /* MatchStatusBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchStatusBadge.swift; path = MatchLiveTv/UI/Components/MatchStatusBadge.swift; sourceTree = "<group>"; };
|
||||
E13972C3F4664A89AE13D5D8 /* AccountScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AccountScreen.swift; path = MatchLiveTv/UI/Account/AccountScreen.swift; sourceTree = "<group>"; };
|
||||
DDF38B8E56394DEBA022F812 /* ForgotPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ForgotPasswordScreen.swift; path = MatchLiveTv/UI/Login/ForgotPasswordScreen.swift; sourceTree = "<group>"; };
|
||||
DA51BC65490E421AA3D16F02 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LoginScreen.swift; path = MatchLiveTv/UI/Login/LoginScreen.swift; sourceTree = "<group>"; };
|
||||
0A439BB96CCB4C038EEF44DD /* MatchesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MatchesScreen.swift; path = MatchLiveTv/UI/Matches/MatchesScreen.swift; sourceTree = "<group>"; };
|
||||
5DD00428364442C6B8C55DDF /* AppNavHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppNavHost.swift; path = MatchLiveTv/UI/Navigation/AppNavHost.swift; sourceTree = "<group>"; };
|
||||
@@ -143,6 +145,8 @@
|
||||
D71CB837FE844932AA590D30 /* MatchScreenScaffold.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7434175C1E849D7A8308896 /* MatchScreenScaffold.swift */; };
|
||||
52806453AD4C4E59BA2F05DE /* MatchSecondaryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DE0F41386A741B2A6FC538B /* MatchSecondaryButton.swift */; };
|
||||
FA601701F8684EE2BFDBBBFD /* MatchStatusBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6A499FE7D94EF498DA9A84 /* MatchStatusBadge.swift */; };
|
||||
A85ED43E903E4983B1438D11 /* AccountScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13972C3F4664A89AE13D5D8 /* AccountScreen.swift */; };
|
||||
195AF301A0AC4BA58F8EDE95 /* ForgotPasswordScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF38B8E56394DEBA022F812 /* ForgotPasswordScreen.swift */; };
|
||||
8C0225C7F87A4F9A8A0C2919 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA51BC65490E421AA3D16F02 /* LoginScreen.swift */; };
|
||||
7293CD3C1B634333A59FA782 /* MatchesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A439BB96CCB4C038EEF44DD /* MatchesScreen.swift */; };
|
||||
465980490F2D417E87241EBA /* AppNavHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DD00428364442C6B8C55DDF /* AppNavHost.swift */; };
|
||||
@@ -176,10 +180,10 @@
|
||||
9246A8532B66443A83535192 /* de.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 85B0F928887C44128434974E; };
|
||||
5DA941A5928A4F638223030C /* es.lproj in Resources */ = {isa = PBXBuildFile; fileRef = E907C350C3E5410FB4A2038D; };
|
||||
B6A69164398F4F9FB84EC475 = {isa = PBXGroup; children = (86BA4622CE11450BAD7902B9, E068E1D4709041789B1E8F5A, 2165E0C34A8B43F28AC2B21C); sourceTree = "<group>"; };
|
||||
86BA4622CE11450BAD7902B9 = {isa = PBXGroup; children = (4FC590728C2F47DE94AEADA3, 69F95F7CAF894139BA244AFF, 5E30D25B02704FAE9C0A4040, A59093CB86BB41FE9DF37182, C00F6C82C9A04A50B1D34BA3, 6F83B04D0BC84879A07C9BFE, 33DEE4675BAB413E9F69549B, 14FE2B5F844A482C91245755, AA9DF2F1C5F04EA198286266, 8EEFEB75DDB04C4986774D4B, C84694361A004982B4007CA5, 1AD71EE7F4A54818A6C427C9, 610902FBCEB7478286C88C08, DD9687B43B5840DD96FAD1C9, 905A6416560A4442AA7D2FCF, 9299E98557E04C5F8DC736F8, 49E1D454901048A58DCE7C41, A90696A40A584C5794938FDA, 2C3F8293D48D44A6A569F452, 347DADA50AE44484ABF19903, 69794703A6D74DB9AA3D16A2, D274E6DA2B7B47C9822FB517, E4BAEE4AB7834E19BA57C4A1, 1060DB32269548BE895EFA7F, CA265A4F2CAA41DA8F68A35C, 0381775B0B1D4378A8BF289A, 85D4CBF80A8F447790235E7C, 2EA97648294C4D98B3D85EFD, FA4A1042B44B41E0AE768E0C, 97DF04B6B21743BC9615E634, 1B031A4D51634E96BBD3D8DA, BC442280384D4194A66F25D2, EF6E8429A9DF475E9C964A2A, AA419130C5A248DCA3B4A016, 8CF76F8D690C405D9FCC4B8E, E1A1C8A803FB4CA1BA741E57, 195EFB33D08D484489FD721B, DB0DF408F60149008C9644C0, E9F0583912AA42F787751A7F, FB19CB746D6146D9ABD1A1C4, CD8C48EA12DB45FF978FA049, 838A41F3BF1E42BDAA2CE2E6, 2074992AD4E94430B9524FB3, B51507FF41FB4B80831D70E3, 8EA7E4972BB74FDD884370D0, CC2B124ACD024538B20863B8, 31B0AEBA3A0140D2BC9698DC, 0C84054AB3574CC5B8056694, D7434175C1E849D7A8308896, 3DE0F41386A741B2A6FC538B, 1F6A499FE7D94EF498DA9A84, DA51BC65490E421AA3D16F02, 0A439BB96CCB4C038EEF44DD, 5DD00428364442C6B8C55DDF, 9F958AB307244896BD8948D2, 21C2A6AB0E164C8CBDF9CF7F, 1702AF57020940DEBF0F9BA0, 3004C07E608644AFB636C0C5, DC39718A2EC74B078A0C5DE9, BCEB22897600469E93762209, 13BC17CE70034FEE97707B21, AAAD219E4FA94CF885472711, 29F84FF3F1E74E49A3EA5101, 1875AE0A77B94DF0921EE457, 3168221266004A33BA35042C, C97603C3A54342799CBD0C03, 5AFEAFB3E8F047B9A2B0DD04, CE7A5A0E6D9A49EA95918E79, 68CB1416AC24491CAA4CE87A, F68654552B254000B6F0BE97, 1255FF9134D246ECBE1DF72C, 6DFD25C0855F482DBEFD8757, 12C3FA28B28146B9A8E3A70A, 5D760AAFB9C34DACBFC2387A, 85B0F928887C44128434974E, E907C350C3E5410FB4A2038D); name = MatchLiveTv; sourceTree = "<group>"; };
|
||||
86BA4622CE11450BAD7902B9 = {isa = PBXGroup; children = (4FC590728C2F47DE94AEADA3, 69F95F7CAF894139BA244AFF, 5E30D25B02704FAE9C0A4040, A59093CB86BB41FE9DF37182, C00F6C82C9A04A50B1D34BA3, 6F83B04D0BC84879A07C9BFE, 33DEE4675BAB413E9F69549B, 14FE2B5F844A482C91245755, AA9DF2F1C5F04EA198286266, 8EEFEB75DDB04C4986774D4B, C84694361A004982B4007CA5, 1AD71EE7F4A54818A6C427C9, 610902FBCEB7478286C88C08, DD9687B43B5840DD96FAD1C9, 905A6416560A4442AA7D2FCF, 9299E98557E04C5F8DC736F8, 49E1D454901048A58DCE7C41, A90696A40A584C5794938FDA, 2C3F8293D48D44A6A569F452, 347DADA50AE44484ABF19903, 69794703A6D74DB9AA3D16A2, D274E6DA2B7B47C9822FB517, E4BAEE4AB7834E19BA57C4A1, 1060DB32269548BE895EFA7F, CA265A4F2CAA41DA8F68A35C, 0381775B0B1D4378A8BF289A, 85D4CBF80A8F447790235E7C, 2EA97648294C4D98B3D85EFD, FA4A1042B44B41E0AE768E0C, 97DF04B6B21743BC9615E634, 1B031A4D51634E96BBD3D8DA, BC442280384D4194A66F25D2, EF6E8429A9DF475E9C964A2A, AA419130C5A248DCA3B4A016, 8CF76F8D690C405D9FCC4B8E, E1A1C8A803FB4CA1BA741E57, 195EFB33D08D484489FD721B, DB0DF408F60149008C9644C0, E9F0583912AA42F787751A7F, FB19CB746D6146D9ABD1A1C4, CD8C48EA12DB45FF978FA049, 838A41F3BF1E42BDAA2CE2E6, 2074992AD4E94430B9524FB3, B51507FF41FB4B80831D70E3, 8EA7E4972BB74FDD884370D0, CC2B124ACD024538B20863B8, 31B0AEBA3A0140D2BC9698DC, 0C84054AB3574CC5B8056694, D7434175C1E849D7A8308896, 3DE0F41386A741B2A6FC538B, 1F6A499FE7D94EF498DA9A84, E13972C3F4664A89AE13D5D8, DDF38B8E56394DEBA022F812, DA51BC65490E421AA3D16F02, 0A439BB96CCB4C038EEF44DD, 5DD00428364442C6B8C55DDF, 9F958AB307244896BD8948D2, 21C2A6AB0E164C8CBDF9CF7F, 1702AF57020940DEBF0F9BA0, 3004C07E608644AFB636C0C5, DC39718A2EC74B078A0C5DE9, BCEB22897600469E93762209, 13BC17CE70034FEE97707B21, AAAD219E4FA94CF885472711, 29F84FF3F1E74E49A3EA5101, 1875AE0A77B94DF0921EE457, 3168221266004A33BA35042C, C97603C3A54342799CBD0C03, 5AFEAFB3E8F047B9A2B0DD04, CE7A5A0E6D9A49EA95918E79, 68CB1416AC24491CAA4CE87A, F68654552B254000B6F0BE97, 1255FF9134D246ECBE1DF72C, 6DFD25C0855F482DBEFD8757, 12C3FA28B28146B9A8E3A70A, 5D760AAFB9C34DACBFC2387A, 85B0F928887C44128434974E, E907C350C3E5410FB4A2038D); name = MatchLiveTv; sourceTree = "<group>"; };
|
||||
E068E1D4709041789B1E8F5A = {isa = PBXGroup; children = (73BB032A0DD74458B0E716C6, C186E88B35A0443494AE6E8E, 5751DB993C52460D960B8067, 0E243C00009048F6A778323C, 634AD5C2E53B4C098582284A, B7DE055717014393B3F62D5A, C3C1AC6562ED4A908D7C8B0A, 610A4D03FA384F0287AAF825); name = MatchLiveTvTests; sourceTree = "<group>"; };
|
||||
2165E0C34A8B43F28AC2B21C = {isa = PBXGroup; children = (6FF24BB96E9C40F6A9F6E25A, 8F679C9C03AE47D6A028DBAC); name = Products; sourceTree = "<group>"; };
|
||||
3B35A518C56047A48825F0F8 = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (F7624153A1C84C218B602FFB, D973F2B503854C6AA61A1559, FE2D8C8359CC4DA189DF5DC6, B607A146AE754F6A819FB186, B0D93B493FD0427CA72F5862, 3C223B19BA2149C5B7E5CC5A, 3D119DE97DE5452D903F364C, 8ABF00BDC15C400A9089FC28, 7D2649B9131F4DDA83F68B72, AB71730B7CA24D3E8667BCFF, ADA58448C2344DADAA90F212, 95F7871858C44379A62012BA, 3FBBEF8DFF5E4F5D9E34C860, 91694276C76B438F8B52448F, 51D849AFAC8D4F57BEF05CAB, D771BABE58A3439CA1768F7F, 23C149E317BD416D9CF0B171, 43C57FF23CB347AC99284468, 8661E674C82545C4AEC0A7CE, E133D004CF0A4FF7A51A47B1, 72090ACA64CB4885BE7AB333, D80261301CA740568D8807D2, 9ECD8B486DF84890BD39B108, C217B8AD49D04976A6D7FC7E, 978AAF829F654537AC473F52, 032C790773A848B7A0434C67, A37BC192D6C9433F83FABAA8, 99F1D8A337EE46C8BDD34F60, F024262D0D0F4B759E1A4F14, FE9E0B76650246E490722317, BD642A0BD19D424985FBF1A3, B48421E7BD5D48E499948E2E, C6207D98BB484493BEDD4BBB, E715CA8C60E6439683E1DA0E, 35F5E2E5FB0D4B9FA475ADA4, 6D8B0F1792E440C19F49AD03, CC563D93928C438AB0B78E99, 9343740F4EB94F609A4D1A51, D71C1DFF234742DA81D23AF7, DADD5004759C409F82D4FEE2, 957381C0C3A447E7A3240ACD, 326AEF0FBFE74201BE70D2DD, DCC6F79099C641DF9EA6F36F, CA82A2382C4A45BE88276507, 37E8D25C18014E0C8139D84D, 67A37D0BB28F4BD1872BACA6, 7AA96446B33C4ECCB90DDFD0, 599290557AE4430EA30582D7, D71CB837FE844932AA590D30, 52806453AD4C4E59BA2F05DE, FA601701F8684EE2BFDBBBFD, 8C0225C7F87A4F9A8A0C2919, 7293CD3C1B634333A59FA782, 465980490F2D417E87241EBA, 044E04EEE71D472EA524C555, 52CEBF8B18834CB8B650FE16, 94ADCCD464DD43149D0C0E60, F23C2967ED7140EB8DCB2D2A, 47AD4ADC4ACE4F6DA7AD708C, 8B0A9A9DBAF346199EBA21B3, D826E98233304F5FB27B27C3, 579A3A8D49DF43E7B11942C6, D0BEA6E726C04A95A8CC0018, 15DA64BB40B14C9B899A3729, 4DF6B21EFDD54DF999385511, AEF63662B6B5454F92986710, 78179BA533074F1ABBB7391E, 45E14846E79845CE9C20C5EC, 2049733B63794135A4BFBB53); runOnlyForDeploymentPostprocessing = 0; };
|
||||
3B35A518C56047A48825F0F8 = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (F7624153A1C84C218B602FFB, D973F2B503854C6AA61A1559, FE2D8C8359CC4DA189DF5DC6, B607A146AE754F6A819FB186, B0D93B493FD0427CA72F5862, 3C223B19BA2149C5B7E5CC5A, 3D119DE97DE5452D903F364C, 8ABF00BDC15C400A9089FC28, 7D2649B9131F4DDA83F68B72, AB71730B7CA24D3E8667BCFF, ADA58448C2344DADAA90F212, 95F7871858C44379A62012BA, 3FBBEF8DFF5E4F5D9E34C860, 91694276C76B438F8B52448F, 51D849AFAC8D4F57BEF05CAB, D771BABE58A3439CA1768F7F, 23C149E317BD416D9CF0B171, 43C57FF23CB347AC99284468, 8661E674C82545C4AEC0A7CE, E133D004CF0A4FF7A51A47B1, 72090ACA64CB4885BE7AB333, D80261301CA740568D8807D2, 9ECD8B486DF84890BD39B108, C217B8AD49D04976A6D7FC7E, 978AAF829F654537AC473F52, 032C790773A848B7A0434C67, A37BC192D6C9433F83FABAA8, 99F1D8A337EE46C8BDD34F60, F024262D0D0F4B759E1A4F14, FE9E0B76650246E490722317, BD642A0BD19D424985FBF1A3, B48421E7BD5D48E499948E2E, C6207D98BB484493BEDD4BBB, E715CA8C60E6439683E1DA0E, 35F5E2E5FB0D4B9FA475ADA4, 6D8B0F1792E440C19F49AD03, CC563D93928C438AB0B78E99, 9343740F4EB94F609A4D1A51, D71C1DFF234742DA81D23AF7, DADD5004759C409F82D4FEE2, 957381C0C3A447E7A3240ACD, 326AEF0FBFE74201BE70D2DD, DCC6F79099C641DF9EA6F36F, CA82A2382C4A45BE88276507, 37E8D25C18014E0C8139D84D, 67A37D0BB28F4BD1872BACA6, 7AA96446B33C4ECCB90DDFD0, 599290557AE4430EA30582D7, D71CB837FE844932AA590D30, 52806453AD4C4E59BA2F05DE, FA601701F8684EE2BFDBBBFD, A85ED43E903E4983B1438D11, 195AF301A0AC4BA58F8EDE95, 8C0225C7F87A4F9A8A0C2919, 7293CD3C1B634333A59FA782, 465980490F2D417E87241EBA, 044E04EEE71D472EA524C555, 52CEBF8B18834CB8B650FE16, 94ADCCD464DD43149D0C0E60, F23C2967ED7140EB8DCB2D2A, 47AD4ADC4ACE4F6DA7AD708C, 8B0A9A9DBAF346199EBA21B3, D826E98233304F5FB27B27C3, 579A3A8D49DF43E7B11942C6, D0BEA6E726C04A95A8CC0018, 15DA64BB40B14C9B899A3729, 4DF6B21EFDD54DF999385511, AEF63662B6B5454F92986710, 78179BA533074F1ABBB7391E, 45E14846E79845CE9C20C5EC, 2049733B63794135A4BFBB53); runOnlyForDeploymentPostprocessing = 0; };
|
||||
729C230D5EB64A76BCD517B4 = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (6B75AFC14BBF4E2B97762ECB, 9BA38C8D635F4770A232AE85, 1889CDB168794D81A5B24699, E25CB4FB944141D0AAB57EDF, 6BE3B2533E6A4E3DA32EA972, B88859D9B34E4F289913DDFC, B8CC446DB3AE4BABB4D739D1, 6CCB67439127403180D9C697); runOnlyForDeploymentPostprocessing = 0; };
|
||||
6B4D5383389142E7AE9C4F41 = {isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = (C44DD19CDB6545A381B61D20, 7C8A2DFFAB6B4CB8AA3E4FE8, 8D479F8873F94073938D737E, 9A894DBC632C4904BC5D9A78, 9246A8532B66443A83535192, 5DA941A5928A4F638223030C); runOnlyForDeploymentPostprocessing = 0; };
|
||||
AA7CFC56BCC040A4AB487E2C = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0; };
|
||||
|
||||
@@ -78,6 +78,23 @@ enum L10n {
|
||||
"action.exit": "Esci",
|
||||
"action.login": "Accedi",
|
||||
"action.logout": "Esci",
|
||||
"account.confirm.password": "Conferma password",
|
||||
"account.current.password": "Password attuale",
|
||||
"account.name.label": "Nome",
|
||||
"account.new.password": "Nuova password (min. 8 caratteri)",
|
||||
"account.password.heading": "Cambia password",
|
||||
"account.password.saved": "Password aggiornata",
|
||||
"account.profile.heading": "Profilo",
|
||||
"account.profile.saved": "Profilo aggiornato",
|
||||
"account.role": "Ruolo: %1$@",
|
||||
"account.save.password": "Aggiorna password",
|
||||
"account.save.profile": "Salva profilo",
|
||||
"account.title": "Account",
|
||||
"forgot.password.lead": "Inserisci l'email dell'account: ti invieremo un link per reimpostare la password.",
|
||||
"forgot.password.submit": "Invia link di reset",
|
||||
"forgot.password.success": "Se l'email è registrata, riceverai a breve un link per reimpostare la password.",
|
||||
"forgot.password.title": "Password dimenticata",
|
||||
"login.forgot.password": "Password dimenticata?",
|
||||
"action.ok": "OK",
|
||||
"action.save": "Salva",
|
||||
"api.error.decoding": "Risposta del server non valida",
|
||||
@@ -352,6 +369,23 @@ enum L10n {
|
||||
"action.exit": "Exit",
|
||||
"action.login": "Log in",
|
||||
"action.logout": "Log out",
|
||||
"account.confirm.password": "Confirm password",
|
||||
"account.current.password": "Current password",
|
||||
"account.name.label": "Name",
|
||||
"account.new.password": "New password (min. 8 characters)",
|
||||
"account.password.heading": "Change password",
|
||||
"account.password.saved": "Password updated",
|
||||
"account.profile.heading": "Profile",
|
||||
"account.profile.saved": "Profile updated",
|
||||
"account.role": "Role: %1$@",
|
||||
"account.save.password": "Update password",
|
||||
"account.save.profile": "Save profile",
|
||||
"account.title": "Account",
|
||||
"forgot.password.lead": "Enter your account email: we'll send you a link to reset your password.",
|
||||
"forgot.password.submit": "Send reset link",
|
||||
"forgot.password.success": "If the email is registered, you will receive a password reset link shortly.",
|
||||
"forgot.password.title": "Forgot password",
|
||||
"login.forgot.password": "Forgot password?",
|
||||
"action.ok": "OK",
|
||||
"action.save": "Save",
|
||||
"api.error.decoding": "Invalid server response",
|
||||
@@ -626,6 +660,23 @@ enum L10n {
|
||||
"action.exit": "Quitter",
|
||||
"action.login": "Connexion",
|
||||
"action.logout": "Déconnexion",
|
||||
"account.confirm.password": "Confirmer le mot de passe",
|
||||
"account.current.password": "Mot de passe actuel",
|
||||
"account.name.label": "Nom",
|
||||
"account.new.password": "Nouveau mot de passe (min. 8 caractères)",
|
||||
"account.password.heading": "Changer le mot de passe",
|
||||
"account.password.saved": "Mot de passe mis à jour",
|
||||
"account.profile.heading": "Profil",
|
||||
"account.profile.saved": "Profil mis à jour",
|
||||
"account.role": "Rôle : %1$@",
|
||||
"account.save.password": "Mettre à jour le mot de passe",
|
||||
"account.save.profile": "Enregistrer le profil",
|
||||
"account.title": "Compte",
|
||||
"forgot.password.lead": "Saisissez l'e-mail du compte : nous vous enverrons un lien pour réinitialiser le mot de passe.",
|
||||
"forgot.password.submit": "Envoyer le lien",
|
||||
"forgot.password.success": "Si l'e-mail est enregistré, vous recevrez bientôt un lien de réinitialisation.",
|
||||
"forgot.password.title": "Mot de passe oublié",
|
||||
"login.forgot.password": "Mot de passe oublié ?",
|
||||
"action.ok": "OK",
|
||||
"action.save": "Enregistrer",
|
||||
"api.error.decoding": "Réponse du serveur non valide",
|
||||
@@ -900,6 +951,23 @@ enum L10n {
|
||||
"action.exit": "Beenden",
|
||||
"action.login": "Anmelden",
|
||||
"action.logout": "Abmelden",
|
||||
"account.confirm.password": "Passwort bestätigen",
|
||||
"account.current.password": "Aktuelles Passwort",
|
||||
"account.name.label": "Name",
|
||||
"account.new.password": "Neues Passwort (mind. 8 Zeichen)",
|
||||
"account.password.heading": "Passwort ändern",
|
||||
"account.password.saved": "Passwort aktualisiert",
|
||||
"account.profile.heading": "Profil",
|
||||
"account.profile.saved": "Profil aktualisiert",
|
||||
"account.role": "Rolle: %1$@",
|
||||
"account.save.password": "Passwort aktualisieren",
|
||||
"account.save.profile": "Profil speichern",
|
||||
"account.title": "Konto",
|
||||
"forgot.password.lead": "Gib die E-Mail des Kontos ein: Wir senden dir einen Link zum Zurücksetzen des Passworts.",
|
||||
"forgot.password.submit": "Reset-Link senden",
|
||||
"forgot.password.success": "Wenn die E-Mail registriert ist, erhältst du in Kürze einen Link zum Zurücksetzen.",
|
||||
"forgot.password.title": "Passwort vergessen",
|
||||
"login.forgot.password": "Passwort vergessen?",
|
||||
"action.ok": "OK",
|
||||
"action.save": "Speichern",
|
||||
"api.error.decoding": "Ungültige Serverantwort",
|
||||
@@ -1174,6 +1242,23 @@ enum L10n {
|
||||
"action.exit": "Salir",
|
||||
"action.login": "Acceder",
|
||||
"action.logout": "Salir",
|
||||
"account.confirm.password": "Confirmar contraseña",
|
||||
"account.current.password": "Contraseña actual",
|
||||
"account.name.label": "Nombre",
|
||||
"account.new.password": "Nueva contraseña (mín. 8 caracteres)",
|
||||
"account.password.heading": "Cambiar contraseña",
|
||||
"account.password.saved": "Contraseña actualizada",
|
||||
"account.profile.heading": "Perfil",
|
||||
"account.profile.saved": "Perfil actualizado",
|
||||
"account.role": "Rol: %1$@",
|
||||
"account.save.password": "Actualizar contraseña",
|
||||
"account.save.profile": "Guardar perfil",
|
||||
"account.title": "Cuenta",
|
||||
"forgot.password.lead": "Introduce el email de la cuenta: te enviaremos un enlace para restablecer la contraseña.",
|
||||
"forgot.password.submit": "Enviar enlace",
|
||||
"forgot.password.success": "Si el email está registrado, recibirás en breve un enlace para restablecer la contraseña.",
|
||||
"forgot.password.title": "Contraseña olvidada",
|
||||
"login.forgot.password": "¿Olvidaste la contraseña?",
|
||||
"action.ok": "OK",
|
||||
"action.save": "Guardar",
|
||||
"api.error.decoding": "Respuesta del servidor no válida",
|
||||
|
||||
@@ -13,6 +13,34 @@ struct RefreshRequest: Encodable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ForgotPasswordRequest: Encodable {
|
||||
let email: String
|
||||
}
|
||||
|
||||
struct ForgotPasswordResponse: Decodable {
|
||||
let message: String
|
||||
}
|
||||
|
||||
struct UpdateAccountRequest: Encodable {
|
||||
let name: String
|
||||
}
|
||||
|
||||
struct ChangePasswordRequest: Encodable {
|
||||
let currentPassword: String
|
||||
let password: String
|
||||
let passwordConfirmation: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case currentPassword = "current_password"
|
||||
case password
|
||||
case passwordConfirmation = "password_confirmation"
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageResponse: Decodable {
|
||||
let message: String
|
||||
}
|
||||
|
||||
struct LoginResponse: Decodable {
|
||||
let user: UserDto
|
||||
let accessToken: String
|
||||
|
||||
@@ -73,6 +73,33 @@ final class MatchLiveAPI: @unchecked Sendable {
|
||||
try await postVoid("auth/logout")
|
||||
}
|
||||
|
||||
func forgotPassword(email: String) async throws -> ForgotPasswordResponse {
|
||||
try await post("auth/password/forgot", body: ForgotPasswordRequest(email: email))
|
||||
}
|
||||
|
||||
func account() async throws -> UserDto {
|
||||
try await get("account")
|
||||
}
|
||||
|
||||
func updateAccount(name: String) async throws -> UserDto {
|
||||
try await patch("account", body: UpdateAccountRequest(name: name))
|
||||
}
|
||||
|
||||
func changePassword(
|
||||
currentPassword: String,
|
||||
password: String,
|
||||
passwordConfirmation: String
|
||||
) async throws -> MessageResponse {
|
||||
try await patch(
|
||||
"account/password",
|
||||
body: ChangePasswordRequest(
|
||||
currentPassword: currentPassword,
|
||||
password: password,
|
||||
passwordConfirmation: passwordConfirmation
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Sports & Teams
|
||||
|
||||
func sports() async throws -> [SportDto] {
|
||||
@@ -267,6 +294,7 @@ final class MatchLiveAPI: @unchecked Sendable {
|
||||
return path.hasSuffix("/auth/login")
|
||||
|| path.hasSuffix("/auth/refresh")
|
||||
|| path.hasSuffix("/auth/register")
|
||||
|| path.hasSuffix("/auth/password/forgot")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,31 @@ final class AuthRepository {
|
||||
return session
|
||||
}
|
||||
|
||||
func forgotPassword(email: String) async throws -> String {
|
||||
try await api.forgotPassword(email: email.trimmingCharacters(in: .whitespacesAndNewlines)).message
|
||||
}
|
||||
|
||||
func fetchAccount() async throws -> User {
|
||||
try await api.account().toDomain()
|
||||
}
|
||||
|
||||
func updateName(_ name: String) async throws -> User {
|
||||
let user = try await api.updateAccount(name: name.trimmingCharacters(in: .whitespacesAndNewlines)).toDomain()
|
||||
if var stored = tokenStore.session {
|
||||
stored = StoredSession(user: user, accessToken: stored.accessToken, refreshToken: stored.refreshToken)
|
||||
tokenStore.saveSession(stored)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func changePassword(currentPassword: String, password: String, passwordConfirmation: String) async throws {
|
||||
_ = try await api.changePassword(
|
||||
currentPassword: currentPassword,
|
||||
password: password,
|
||||
passwordConfirmation: passwordConfirmation
|
||||
)
|
||||
}
|
||||
|
||||
func logout() async {
|
||||
try? await api.logout()
|
||||
tokenStore.clear()
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AccountScreen: View {
|
||||
@ObservedObject var container: AppContainer
|
||||
let onBack: () -> Void
|
||||
let onLoggedOut: () -> Void
|
||||
|
||||
@State private var email = ""
|
||||
@State private var role = ""
|
||||
@State private var name = ""
|
||||
@State private var currentPassword = ""
|
||||
@State private var newPassword = ""
|
||||
@State private var confirmPassword = ""
|
||||
@State private var passwordVisible = false
|
||||
@State private var loadingProfile = true
|
||||
@State private var savingProfile = false
|
||||
@State private var savingPassword = false
|
||||
@State private var loggingOut = false
|
||||
@State private var profileMessage: String?
|
||||
@State private var profileError: String?
|
||||
@State private var passwordMessage: String?
|
||||
@State private var passwordError: String?
|
||||
@State private var languageTick = 0
|
||||
|
||||
var body: some View {
|
||||
MatchScreenScaffold(
|
||||
topBar: {
|
||||
HStack {
|
||||
Button(action: onBack) {
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
}
|
||||
Text(L10n.t("account.title"))
|
||||
.font(MatchTypography.headlineMedium)
|
||||
.foregroundStyle(.white)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
},
|
||||
content: {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Text(L10n.t("account.profile.heading"))
|
||||
.font(MatchTypography.titleMedium)
|
||||
.foregroundStyle(.white)
|
||||
|
||||
disabledField(title: L10n.t("login.email"), value: email)
|
||||
if !role.isEmpty {
|
||||
Text(L10n.t("account.role", role))
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
}
|
||||
MatchAccountTextField(title: L10n.t("account.name.label"), text: $name)
|
||||
|
||||
if let profileError {
|
||||
Text(profileError).foregroundStyle(MatchColors.primaryRed)
|
||||
}
|
||||
if let profileMessage {
|
||||
Text(profileMessage).foregroundStyle(MatchColors.textSecondary)
|
||||
}
|
||||
|
||||
MatchPrimaryButton(
|
||||
label: L10n.t("account.save.profile"),
|
||||
action: saveProfile,
|
||||
enabled: !loadingProfile && !name.trimmingCharacters(in: .whitespaces).isEmpty && !savingProfile,
|
||||
loading: savingProfile
|
||||
)
|
||||
|
||||
Text(L10n.t("account.password.heading"))
|
||||
.font(MatchTypography.titleMedium)
|
||||
.foregroundStyle(.white)
|
||||
.padding(.top, 16)
|
||||
|
||||
MatchAccountSecureField(
|
||||
title: L10n.t("account.current.password"),
|
||||
text: $currentPassword,
|
||||
visible: $passwordVisible
|
||||
)
|
||||
MatchAccountSecureField(
|
||||
title: L10n.t("account.new.password"),
|
||||
text: $newPassword,
|
||||
visible: $passwordVisible
|
||||
)
|
||||
MatchAccountSecureField(
|
||||
title: L10n.t("account.confirm.password"),
|
||||
text: $confirmPassword,
|
||||
visible: $passwordVisible
|
||||
)
|
||||
|
||||
if let passwordError {
|
||||
Text(passwordError).foregroundStyle(MatchColors.primaryRed)
|
||||
}
|
||||
if let passwordMessage {
|
||||
Text(passwordMessage).foregroundStyle(MatchColors.textSecondary)
|
||||
}
|
||||
|
||||
MatchPrimaryButton(
|
||||
label: L10n.t("account.save.password"),
|
||||
action: savePassword,
|
||||
enabled: !currentPassword.isEmpty && !newPassword.isEmpty && !confirmPassword.isEmpty && !savingPassword,
|
||||
loading: savingPassword
|
||||
)
|
||||
|
||||
MatchSecondaryButton(
|
||||
label: L10n.t("action.logout"),
|
||||
action: logout,
|
||||
enabled: !loggingOut
|
||||
)
|
||||
.padding(.top, 24)
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
}
|
||||
)
|
||||
.task { await loadAccount() }
|
||||
.onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in
|
||||
languageTick += 1
|
||||
}
|
||||
.id(languageTick)
|
||||
}
|
||||
|
||||
private func disabledField(title: String, value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(title)
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
Text(value.isEmpty ? "—" : value)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(12)
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline))
|
||||
}
|
||||
}
|
||||
|
||||
private func loadAccount() async {
|
||||
loadingProfile = true
|
||||
do {
|
||||
let user = try await container.authRepository.fetchAccount()
|
||||
email = user.email
|
||||
name = user.name
|
||||
role = user.role
|
||||
} catch {
|
||||
profileError = UserFacingError.message(for: error) ?? L10n.t("common.error.generic")
|
||||
}
|
||||
loadingProfile = false
|
||||
}
|
||||
|
||||
private func saveProfile() {
|
||||
savingProfile = true
|
||||
profileError = nil
|
||||
profileMessage = nil
|
||||
Task {
|
||||
do {
|
||||
let user = try await container.authRepository.updateName(name)
|
||||
name = user.name
|
||||
profileMessage = L10n.t("account.profile.saved")
|
||||
} catch {
|
||||
profileError = UserFacingError.message(for: error) ?? L10n.t("common.error.generic")
|
||||
}
|
||||
savingProfile = false
|
||||
}
|
||||
}
|
||||
|
||||
private func savePassword() {
|
||||
savingPassword = true
|
||||
passwordError = nil
|
||||
passwordMessage = nil
|
||||
Task {
|
||||
do {
|
||||
try await container.authRepository.changePassword(
|
||||
currentPassword: currentPassword,
|
||||
password: newPassword,
|
||||
passwordConfirmation: confirmPassword
|
||||
)
|
||||
currentPassword = ""
|
||||
newPassword = ""
|
||||
confirmPassword = ""
|
||||
passwordMessage = L10n.t("account.password.saved")
|
||||
} catch {
|
||||
passwordError = UserFacingError.message(for: error) ?? L10n.t("common.error.generic")
|
||||
}
|
||||
savingPassword = false
|
||||
}
|
||||
}
|
||||
|
||||
private func logout() {
|
||||
loggingOut = true
|
||||
Task {
|
||||
await container.authRepository.logout()
|
||||
onLoggedOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MatchAccountTextField: View {
|
||||
let title: String
|
||||
@Binding var text: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(title)
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
TextField("", text: $text)
|
||||
.padding(12)
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MatchAccountSecureField: View {
|
||||
let title: String
|
||||
@Binding var text: String
|
||||
@Binding var visible: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(title)
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
HStack {
|
||||
Group {
|
||||
if visible {
|
||||
TextField("", text: $text)
|
||||
} else {
|
||||
SecureField("", text: $text)
|
||||
}
|
||||
}
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
Button(action: { visible.toggle() }) {
|
||||
Image(systemName: visible ? "eye.slash" : "eye")
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ForgotPasswordScreen: View {
|
||||
@ObservedObject var container: AppContainer
|
||||
let onBack: () -> Void
|
||||
|
||||
@State private var email = ""
|
||||
@State private var loading = false
|
||||
@State private var error: String?
|
||||
@State private var success = false
|
||||
@State private var languageTick = 0
|
||||
|
||||
var body: some View {
|
||||
MatchScreenScaffold(
|
||||
topBar: {
|
||||
HStack {
|
||||
Button(action: onBack) {
|
||||
Image(systemName: "chevron.left")
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
}
|
||||
Text(L10n.t("forgot.password.title"))
|
||||
.font(MatchTypography.headlineMedium)
|
||||
.foregroundStyle(.white)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
},
|
||||
content: {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
Text(L10n.t("forgot.password.lead"))
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(L10n.t("login.email"))
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
TextField("", text: $email)
|
||||
.textInputAutocapitalization(.never)
|
||||
.keyboardType(.emailAddress)
|
||||
.autocorrectionDisabled()
|
||||
.disabled(success)
|
||||
.padding(12)
|
||||
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline))
|
||||
}
|
||||
|
||||
if let error {
|
||||
Text(error)
|
||||
.foregroundStyle(MatchColors.primaryRed)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
if success {
|
||||
Text(L10n.t("forgot.password.success"))
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
MatchPrimaryButton(
|
||||
label: L10n.t("forgot.password.submit"),
|
||||
action: submit,
|
||||
enabled: !email.trimmingCharacters(in: .whitespaces).isEmpty && !loading && !success,
|
||||
loading: loading
|
||||
)
|
||||
}
|
||||
.padding(24)
|
||||
}
|
||||
}
|
||||
)
|
||||
.onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in
|
||||
languageTick += 1
|
||||
}
|
||||
.id(languageTick)
|
||||
}
|
||||
|
||||
private func submit() {
|
||||
loading = true
|
||||
error = nil
|
||||
Task {
|
||||
do {
|
||||
_ = try await container.authRepository.forgotPassword(email: email)
|
||||
success = true
|
||||
} catch {
|
||||
self.error = UserFacingError.message(for: error) ?? L10n.t("common.error.generic")
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import SwiftUI
|
||||
struct LoginScreen: View {
|
||||
@ObservedObject var container: AppContainer
|
||||
let onLoggedIn: () -> Void
|
||||
let onForgotPassword: () -> Void
|
||||
|
||||
@State private var email = ""
|
||||
@State private var password = ""
|
||||
@@ -37,6 +38,16 @@ struct LoginScreen: View {
|
||||
MatchSecureField(title: L10n.t("login.password"), text: $password, visible: $passwordVisible)
|
||||
}
|
||||
.padding(.top, 32)
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: onForgotPassword) {
|
||||
Text(L10n.t("login.forgot.password"))
|
||||
.font(MatchTypography.bodyMedium)
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
.underline()
|
||||
}
|
||||
}
|
||||
.padding(.top, 12)
|
||||
if let error {
|
||||
Text(error)
|
||||
.foregroundStyle(MatchColors.primaryRed)
|
||||
|
||||
@@ -5,7 +5,7 @@ struct MatchesScreen: View {
|
||||
var refreshToken: Int = 0
|
||||
let onOpenSetup: (String) -> Void
|
||||
let onOpenBroadcast: (String) -> Void
|
||||
let onLogout: () -> Void
|
||||
let onOpenAccount: () -> Void
|
||||
|
||||
@State private var loading = true
|
||||
@State private var refreshing = false
|
||||
@@ -37,15 +37,12 @@ struct MatchesScreen: View {
|
||||
}
|
||||
.accessibilityLabel(L10n.t("language.label"))
|
||||
Button {
|
||||
Task {
|
||||
await container.authRepository.logout()
|
||||
onLogout()
|
||||
}
|
||||
onOpenAccount()
|
||||
} label: {
|
||||
Image(systemName: "rectangle.portrait.and.arrow.right")
|
||||
Image(systemName: "person.crop.circle")
|
||||
.foregroundStyle(MatchColors.textSecondary)
|
||||
}
|
||||
.accessibilityLabel(L10n.t("action.logout"))
|
||||
.accessibilityLabel(L10n.t("account.title"))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
|
||||
@@ -27,7 +27,16 @@ struct AppNavHost: View {
|
||||
case .splash:
|
||||
EmptyView()
|
||||
case .login:
|
||||
LoginScreen(container: container) { path = [.matches] }
|
||||
LoginScreen(
|
||||
container: container,
|
||||
onLoggedIn: { path = [.matches] },
|
||||
onForgotPassword: { path.append(.forgotPassword) }
|
||||
)
|
||||
case .forgotPassword:
|
||||
ForgotPasswordScreen(
|
||||
container: container,
|
||||
onBack: { path.removeLast() }
|
||||
)
|
||||
case .matches:
|
||||
MatchesScreen(
|
||||
container: container,
|
||||
@@ -38,9 +47,15 @@ struct AppNavHost: View {
|
||||
onOpenBroadcast: { sessionId in
|
||||
broadcastRoute = BroadcastRoute(sessionId: sessionId)
|
||||
},
|
||||
onLogout: { path = [.login] }
|
||||
onOpenAccount: { path.append(.account) }
|
||||
)
|
||||
.lockPortraitOrientation()
|
||||
case .account:
|
||||
AccountScreen(
|
||||
container: container,
|
||||
onBack: { path.removeLast() },
|
||||
onLoggedOut: { path = [.login] }
|
||||
)
|
||||
case .setup, .broadcast:
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ import Foundation
|
||||
enum Routes: Hashable {
|
||||
case splash
|
||||
case login
|
||||
case forgotPassword
|
||||
case matches
|
||||
case account
|
||||
case setup(matchId: String, step: Int)
|
||||
case broadcast(sessionId: String)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user