Integrate email notifications into signup and login (#38)

This commit is contained in:
Stephan D committed 2026-09-12 15:07:00 +02:00
1 parent 326d58f705
commit ad5a061164
55 files changed
+1859 -3464

No files matched your search

+1
View File
@@ -19,6 +19,7 @@ class AuthFeedback extends StatelessWidget {
? (signUp ? l.signupPreview : l.loginPreview)
: l.authCompleted)
: switch (error) {
AuthError.accountNotVerified => l.authVerifyEmail,
AuthError.invalidCredentials => l.authInvalidCredentials,
AuthError.emailInUse => l.authEmailInUse,
AuthError.unavailable => l.authUnavailable,
+10 -4
View File
@@ -104,6 +104,11 @@ class _AuthFormState extends State<AuthForm> {
locale: locale,
);
if (!mounted || _signUp) return;
if (login.state.error == AuthError.accountNotVerified) {
_password.clear();
context.goNamed(AppRoutes.verificationSent, extra: _email.text.trim());
return;
}
final challenge = login.state.result?.challenge;
if (challenge != null) {
_password.clear();
@@ -329,10 +334,11 @@ class _AuthFormState extends State<AuthForm> {
AuthFeedback(state: state, signUp: _signUp),
],
const SizedBox(height: 20),
TextButton(
onPressed: () => context.go('/accounts'),
child: Text(l.wEnter),
),
if (context.read<TwoFactorService>().isSimulated)
TextButton(
onPressed: () => context.go('/accounts'),
child: Text(l.wEnter),
),
Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
File diff suppressed because it is too large. Load diff
@@ -1,860 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'SXCHNGE · Your next move';
@override
String get nextChapter => 'YOUR NEXT CHAPTER';
@override
String get loginHeading => 'Hey, welcome\nback.';
@override
String get signupHeading => 'Make your\nfirst move.';
@override
String get loginIntro => 'Your next move is just a login away.';
@override
String get signupIntro => 'A new account. A world of possibility.';
@override
String get login => 'Log in';
@override
String get signup => 'Sign up';
@override
String get name => 'Your name';
@override
String get email => 'Email address';
@override
String get emailHint => 'you@example.com';
@override
String get password => 'Password';
@override
String get confirmPassword => 'Confirm password';
@override
String get passwordHint => 'Use at least 12 characters.';
@override
String get showPassword => 'Show password';
@override
String get hidePassword => 'Hide password';
@override
String get showConfirmation => 'Show confirmation';
@override
String get hideConfirmation => 'Hide confirmation';
@override
String get createAccount => 'Create account';
@override
String get letsGo => 'Let’s go';
@override
String get loginPreview => 'You’re signed in.';
@override
String get signupPreview => 'Your account is ready.';
@override
String get alreadyMember => 'Already part of sxchnge?';
@override
String get newHere => 'New around here?';
@override
String get loginInstead => 'Log in instead';
@override
String get createAnAccount => 'Create an account';
@override
String get brandEyebrow => 'A LITTLE CHANGE. A BIG NEXT STEP.';
@override
String get brandHeading => 'Good moves.\nStart here.';
@override
String get brandIntro => 'A fresh way forward.\nMake it yours with sxchnge.';
@override
String get brandCard => 'Less friction.\nMore possibility.';
@override
String get brandFooter => 'YOUR NEXT MOVE STARTS WITH YOU.';
@override
String get emailRequired => 'Enter your email address.';
@override
String get emailInvalid => 'Enter a valid email address.';
@override
String get nameRequired => 'Enter your name.';
@override
String get passwordRequired => 'Enter your password.';
@override
String get confirmationRequired => 'Confirm your password.';
@override
String get passwordMismatch => 'Passwords do not match.';
@override
String get language => 'Language';
@override
String get preferenceNotSaved =>
'Language changed for this session. Your preference could not be saved.';
@override
String get authWorking => 'Please wait…';
@override
String get authCompleted => 'Request completed.';
@override
String get authInvalidCredentials =>
'Email or password is incorrect. Please try again.';
@override
String get authEmailInUse =>
'This email is already registered. Try logging in.';
@override
String get authUnavailable => 'The service is unavailable. Please try again.';
@override
String get authUnexpected => 'Something went wrong. Please try again.';
@override
String get checkInbox => 'Check your inbox.';
@override
String get verificationSent =>
'We’ve sent you a verification email. Open the link in your inbox to confirm your email address.';
@override
String get verificationSending => 'Preparing your verification email…';
@override
String get verificationChecking => 'Verifying your email…';
@override
String get verificationVerified => 'Your email\nis verified.';
@override
String get verificationVerifiedBody =>
'One step done. You can now return to login. Identity verification is a separate step.';
@override
String get verificationExpired => 'This link has expired.';
@override
String get verificationExpiredBody =>
'Request a new verification email and use the latest link.';
@override
String get verificationInvalid => 'This link isn’t valid.';
@override
String get verificationInvalidBody =>
'The confirmation link is missing or invalid. Open the complete link from your email, or request a new one.';
@override
String get verificationInternal => 'Something went wrong.';
@override
String get verificationInternalBody =>
'We couldn’t complete your request because of an internal error. Please try again.';
@override
String get verificationResend => 'Resend verification email';
@override
String get verificationRequest => 'Send a new verification email';
@override
String get verificationRetry => 'Retry verification';
@override
String get verificationBack => 'Back to login';
@override
String get verificationHint =>
'No email? Check your spam folder or request another below.';
@override
String get verificationDemoLink => 'Continue to confirmation';
@override
String get verificationStep => 'CONFIRM YOUR EMAIL';
@override
String get verificationRecipient => 'Recipient';
@override
String get verificationChangeEmail => 'Change email';
@override
String get kycTitle => 'Verification & limits';
@override
String get kycIntro =>
'Your account, at your pace. Verify your identity when you need higher limits.';
@override
String get kycUnverified => 'Not verified';
@override
String get kycPending => 'In review';
@override
String get kycApproved => 'Identity verified';
@override
String get kycChanges => 'New photos needed';
@override
String get kycDenied => 'Verification declined';
@override
String get kycUnverifiedBody =>
'You can keep using your account with basic limits. Submit a passport photo and a selfie holding it to request an upgrade.';
@override
String get kycPendingBody =>
'Your submission is awaiting a human review. Basic limits remain active until approval. You do not need to submit again.';
@override
String get kycApprovedBody =>
'Your review is complete. Check your current limits below.';
@override
String get kycChangesBody =>
'Your account remains available with basic limits. Replace the photos and submit a new review request.';
@override
String get kycDeniedBody =>
'Basic limits remain available. If you believe this decision is incorrect, contact support for a review.';
@override
String get kycPassportReason =>
'The passport details could not be read. Retake the photo in good light, with every edge visible.';
@override
String get kycSelfieReason =>
'Your face or passport was not clear in the selfie. Keep both visible and in focus.';
@override
String get kycExpiredReason =>
'The passport has expired. Use a valid passport and take both photos again.';
@override
String get kycDeniedReason =>
'We could not verify your identity from this submission.';
@override
String get kycStart => 'Verify identity';
@override
String get kycResubmit => 'Replace photos';
@override
String get kycRefresh => 'Refresh status & limits';
@override
String get kycSupport => 'Contact support';
@override
String get kycSupportBody =>
'Support is currently unavailable. Please try again later.';
@override
String get kycBasic => 'Basic';
@override
String get kycVerifiedTier => 'Verified';
@override
String get kycLimits => 'Your limits';
@override
String get kycDay => 'Daily volume';
@override
String get kycMonth => 'Monthly volume';
@override
String get kycCurrent => 'Current cap';
@override
String get kycRemaining => 'Available';
@override
String get kycAfter => 'After approval';
@override
String get kycLimitsError =>
'Limits could not be refreshed. Any values below are out of date. Retry to see your current access.';
@override
String get kycLimitsEmpty =>
'No limits were returned. Refresh or check with support before making a transaction.';
@override
String get kycLoading => 'Loading your status…';
@override
String get kycPassport => 'Passport photo';
@override
String get kycSelfie => 'Selfie with passport';
@override
String get kycReview => 'Review your photos';
@override
String get kycPassportHelp =>
'Open your valid passport to the photo page. Show all four edges and keep text sharp. Avoid glare, blur and covered details.';
@override
String get kycSelfieHelp =>
'Hold the open passport beside your face. Show your whole face and the photo page in the same image. Keep both sharp and uncovered.';
@override
String get kycFileRules =>
'JPEG or PNG · up to 10 MB · at least 600 × 600 pixels. Take a photo with your camera, then choose it here.';
@override
String get kycChoose => 'Choose photo';
@override
String get kycReplace => 'Choose a different photo';
@override
String get kycRemove => 'Remove photo';
@override
String get kycNext => 'Continue';
@override
String get kycBack => 'Back';
@override
String get kycSkip => 'Keep basic limits';
@override
String get kycClose => 'Back to account';
@override
String get kycReviewHelp =>
'Check that both photos are readable and show the same passport. A staff member will review them; submission does not immediately raise your limits.';
@override
String get kycConfirm => 'These photos show me and my own valid passport.';
@override
String get kycSubmit => 'Submit for review';
@override
String get kycWorking => 'Processing… Please keep this page open.';
@override
String get kycDiscardTitle => 'Discard these photos?';
@override
String get kycDiscardBody =>
'Your draft is only in memory. Leaving will remove the selected photos; you can start again later.';
@override
String get kycStay => 'Keep editing';
@override
String get kycDiscard => 'Discard & leave';
@override
String get kycErrorUnavailable =>
'The service is unavailable. Your selected photos are still here. Try again.';
@override
String get kycErrorInternal =>
'Something went wrong. Try again; your selected photos have been kept.';
@override
String get kycErrorPermission =>
'We could not open that photo. Check file access or choose another image.';
@override
String get kycErrorFormat =>
'Choose a JPEG or PNG image. Other formats are not supported.';
@override
String get kycErrorLarge =>
'This image is too large. Choose one under 10 MB and 40 megapixels.';
@override
String get kycErrorUnreadable =>
'This image is damaged or cannot be read. Choose another photo.';
@override
String get kycErrorSmall =>
'The photo is too small. Use an image at least 600 pixels wide and high.';
@override
String get kycErrorIncomplete =>
'Choose both photos and confirm they belong to you before submitting.';
@override
String get kycErrorSession =>
'Your session has expired. Return to login, then check your verification status.';
@override
String get kycErrorRate =>
'Too many requests. Wait a moment before trying again.';
@override
String get kycErrorConflict =>
'Your review status has changed. Refresh it before continuing.';
@override
String get kycDemoControls => 'Review scenarios';
@override
String get kycDemoApprove => 'Approve';
@override
String get kycDemoCorrect => 'Request clearer passport';
@override
String get kycDemoSelfie => 'Request clearer selfie';
@override
String get kycDemoExpired => 'Passport expired';
@override
String get kycDemoDeny => 'Decline';
@override
String get kycPreview => 'Explore verification & limits';
@override
String get kycChooseFirst => 'Choose a photo to continue.';
@override
String get kycStatusStale =>
'Status could not be refreshed. The status shown is the last known result.';
@override
String get kycUsed => 'Used';
@override
String get kycReserved => 'Reserved';
@override
String get kycUsageHelp =>
'Used is completed activity. Reserved is held for pending transactions. Both count toward your limit.';
@override
String get kycOccupied => 'Used + reserved';
@override
String get kycNoCapacity => 'No capacity available';
@override
String get wAccounts => 'Accounts';
@override
String get wActivity => 'Activity';
@override
String get wLimits => 'Limits';
@override
String get wSettings => 'Settings';
@override
String get wHeadline => 'Your money.\nYour next move.';
@override
String get wSubheading => 'Top up. Exchange. Go further.';
@override
String get wAvailable => 'Available to you';
@override
String get wTopUp => 'Top up';
@override
String get wExchange => 'Exchange';
@override
String get wPayout => 'Pay out';
@override
String get wRecent => 'Recent activity';
@override
String get wSeeAll => 'See all';
@override
String get wEmptyActivity =>
'Your next move starts here. Your transactions will appear below.';
@override
String get wEmptyAccounts =>
'Your accounts are not ready yet. Try refreshing.';
@override
String get wRefresh => 'Refresh';
@override
String get wSource => 'From account';
@override
String get wDestination => 'To account';
@override
String get wReceiveAccount => 'Receiving account';
@override
String get wAmount => 'Amount';
@override
String get wRecipient => 'Recipient';
@override
String get wRecipientHint => 'Recipient name or reference';
@override
String get wReview => 'Review';
@override
String get wReviewTitle => 'One last look.';
@override
String get wFee => 'Fee';
@override
String get wTotalDebit => 'Leaves your account';
@override
String get wCredit => 'Arrives in your account';
@override
String get wRecipientGets => 'Recipient receives';
@override
String get wYouPay => 'You pay';
@override
String get wRate => 'Exchange rate';
@override
String get wQuoteNotice =>
'These terms are valid for 60 seconds. Confirm them before continuing.';
@override
String get wExpired =>
'This quote expired. Refresh it and review the new terms.';
@override
String get wNewQuote => 'Refresh quote';
@override
String get wConfirm => 'Confirm transaction';
@override
String get wEdit => 'Edit details';
@override
String get wClose => 'Close';
@override
String get wWorking => 'Working on it…';
@override
String get wPending => 'Processing';
@override
String get wCompleted => 'Completed';
@override
String get wFailed => 'Not completed';
@override
String get wPendingTopUp =>
'Your top-up is processing. Your available balance has not changed yet.';
@override
String get wPendingExchange =>
'The amount and fee have left your available balance. The exchanged amount will appear in the receiving account when completed.';
@override
String get wPendingPayout =>
'The amount and fee have left your available balance. The payout is still processing.';
@override
String get wCompleteTopUp =>
'Your account has been credited. The money is available to use.';
@override
String get wCompleteExchange =>
'Your exchange is complete. The money is available in the receiving account.';
@override
String get wCompletePayout =>
'Your payout is complete. The amount and fee have been deducted from your account.';
@override
String get wFailedTopUp =>
'The top-up did not complete. No money was added to your account.';
@override
String get wFailedOutgoing =>
'The transaction did not complete. The amount and fee are available in your account again.';
@override
String get wUnavailable =>
'Could not refresh. The displayed information may be out of date. Try again.';
@override
String get wInvalidAmount =>
'Enter an amount greater than zero, using the currency’s decimal precision.';
@override
String get wInsufficient =>
'Not enough available money for this amount and fee. Top up or enter a smaller amount.';
@override
String get wLimitError =>
'This amount exceeds your available limit. Check your limits before continuing.';
@override
String get wRecipientError =>
'Enter a sample recipient between 6 and 140 characters.';
@override
String get wUnsupported =>
'This combination is unavailable. Choose another account.';
@override
String get wNotFound =>
'This item could not be found. Return to your accounts.';
@override
String get wUncertain =>
'We could not confirm the outcome. Check it before trying another transaction.';
@override
String get wCheckOutcome => 'Check outcome';
@override
String get wNoSubmission =>
'No transaction was found. You can review and confirm again.';
@override
String get wDiscardTitle => 'Leave this transaction?';
@override
String get wDiscardBody =>
'Your entered details will be cleared. No new transaction will be submitted.';
@override
String get wStay => 'Keep editing';
@override
String get wAll => 'All';
@override
String get wSearch => 'Search by reference';
@override
String get wNoMatches => 'No matching transactions. Try another filter.';
@override
String get wClearFilters => 'Clear filters';
@override
String get wReference => 'Reference';
@override
String get wWhen => 'Created';
@override
String get wViewAccount => 'View account';
@override
String get wPreferences => 'Make it yours.';
@override
String get wLanguage => 'Account language';
@override
String get wLanguageHelp => 'Used throughout your account.';
@override
String get wLanguageSaved => 'Saved to your account.';
@override
String get wLanguageFailed =>
'Changed for this session, but not saved to your account. Retry below.';
@override
String get wProfileError =>
'Account preferences could not be loaded. Try again.';
@override
String get wSecurity => 'Security';
@override
String get wSecurityBody =>
'Password and security settings are not available yet.';
@override
String get wIdentity => 'Identity verification';
@override
String get wIdentityHelp => 'Optional verification for higher limits.';
@override
String get wBackLogin => 'Back to login';
@override
String get wEnter => 'Explore your accounts';
@override
String get wUsage => 'Used this period';
@override
String get wLimitsScope => 'Outgoing activity · EUR equivalent';
@override
String get wPendingHint =>
'Refresh to check the latest status. You can keep using your other accounts.';
@override
String get wFundsHeading => 'Where is my money?';
@override
String get wAnother => 'Make another transaction';
@override
String get factorTitle => 'One more check.';
@override
String get factorLogin => 'Confirm it’s you to finish signing in.';
@override
String get factorMoney =>
'Confirm this transaction. No money moves before confirmation.';
@override
String get factorEnableHelp =>
'Confirm to turn on two-factor verification for sign-in and money operations.';
@override
String get factorDisableHelp =>
'Confirm to turn off two-factor verification. Sign-in and money operations will no longer ask for a second code.';
@override
String get factorEmail => 'Enter the six-digit code sent to your email.';
@override
String get factorAuthenticator =>
'Enter the current six-digit code from your authenticator app.';
@override
String get factorCode => 'Verification code';
@override
String get factorConfirm => 'Verify and continue';
@override
String get factorResend => 'Send a new code';
@override
String get factorHelp =>
'Check your inbox and spam folder. A new code replaces the previous one.';
@override
String get factorAppHelp =>
'Use the current code. If you cannot access your authenticator, cancel this action.';
@override
String get factorInvalid =>
'That code is not valid. Check all six digits and try again.';
@override
String get factorExpired =>
'This verification has expired. Request a new code or return to start again.';
@override
String get factorLocked =>
'Too many incorrect attempts. Wait five minutes, then start verification again.';
@override
String get factorWait => 'Please wait before requesting another code.';
@override
String get factorRequired =>
'Verification is needed again. Please confirm this action once more.';
@override
String get factorUnavailable =>
'Could not complete verification. Try again or cancel this action.';
@override
String factorCooldown(int seconds) {
return 'Send a new code in ${seconds}s';
}
@override
String get factorSettings => 'Two-factor verification';
@override
String get factorSettingsHelp =>
'Add a verification code when you sign in, top up, exchange or pay out.';
@override
String get factorOn => 'On';
@override
String get factorOff => 'Off';
@override
String get factorEnable => 'Turn on';
@override
String get factorDisable => 'Turn off';
@override
String get factorEmailMethod => 'Email code';
@override
String get factorAppMethod => 'Authenticator app';
@override
String get factorAppExpired =>
'This verification has expired. Return and start again.';
}
@@ -1,857 +0,0 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Russian (`ru`).
class AppLocalizationsRu extends AppLocalizations {
AppLocalizationsRu([String locale = 'ru']) : super(locale);
@override
String get appTitle => 'SXCHNGE · Ваш следующий шаг';
@override
String get nextChapter => 'НОВАЯ ГЛАВА';
@override
String get loginHeading => 'Рады видеть\nвас снова.';
@override
String get signupHeading => 'Сделайте\nпервый шаг.';
@override
String get loginIntro => 'Войдите, чтобы сделать следующий шаг.';
@override
String get signupIntro => 'Новый аккаунт. Новые возможности.';
@override
String get login => 'Войти';
@override
String get signup => 'Регистрация';
@override
String get name => 'Ваше имя';
@override
String get email => 'Электронная почта';
@override
String get emailHint => 'you@example.com';
@override
String get password => 'Пароль';
@override
String get confirmPassword => 'Повторите пароль';
@override
String get passwordHint => 'Используйте не менее 12 символов.';
@override
String get showPassword => 'Показать пароль';
@override
String get hidePassword => 'Скрыть пароль';
@override
String get showConfirmation => 'Показать повтор пароля';
@override
String get hideConfirmation => 'Скрыть повтор пароля';
@override
String get createAccount => 'Создать аккаунт';
@override
String get letsGo => 'Продолжить';
@override
String get loginPreview => 'Вы вошли в аккаунт.';
@override
String get signupPreview => 'Ваш аккаунт готов.';
@override
String get alreadyMember => 'Уже есть аккаунт sxchnge?';
@override
String get newHere => 'Впервые здесь?';
@override
String get loginInstead => 'Войти в аккаунт';
@override
String get createAnAccount => 'Зарегистрироваться';
@override
String get brandEyebrow => 'МАЛЕНЬКАЯ ПЕРЕМЕНА. БОЛЬШОЙ ШАГ.';
@override
String get brandHeading => 'Всё начинается\nс перемен.';
@override
String get brandIntro =>
'Новый путь вперёд.\nОткройте его вместе со sxchnge.';
@override
String get brandCard => 'Меньше преград.\nБольше возможностей.';
@override
String get brandFooter => 'СЛЕДУЮЩИЙ ШАГ — ЗА ВАМИ.';
@override
String get emailRequired => 'Введите электронную почту.';
@override
String get emailInvalid => 'Введите корректный адрес почты.';
@override
String get nameRequired => 'Введите ваше имя.';
@override
String get passwordRequired => 'Введите пароль.';
@override
String get confirmationRequired => 'Повторите пароль.';
@override
String get passwordMismatch => 'Пароли не совпадают.';
@override
String get language => 'Язык';
@override
String get preferenceNotSaved =>
'Язык изменён для этого сеанса. Сохранить выбор не удалось.';
@override
String get authWorking => 'Подождите…';
@override
String get authCompleted => 'Запрос выполнен.';
@override
String get authInvalidCredentials =>
'Неверная почта или пароль. Попробуйте ещё раз.';
@override
String get authEmailInUse =>
'Эта почта уже зарегистрирована. Попробуйте войти.';
@override
String get authUnavailable => 'Сервис недоступен. Попробуйте ещё раз.';
@override
String get authUnexpected => 'Что-то пошло не так. Попробуйте ещё раз.';
@override
String get checkInbox => 'Проверьте почту.';
@override
String get verificationSent =>
'Мы отправили вам письмо для подтверждения. Откройте ссылку в письме, чтобы подтвердить электронную почту.';
@override
String get verificationSending => 'Готовим письмо для подтверждения…';
@override
String get verificationChecking => 'Подтверждаем вашу почту…';
@override
String get verificationVerified => 'Ваша почта\nподтверждена.';
@override
String get verificationVerifiedBody =>
'Первый шаг пройден. Теперь можно вернуться ко входу. Подтверждение личности — отдельный этап.';
@override
String get verificationExpired => 'Срок действия ссылки истёк.';
@override
String get verificationExpiredBody =>
'Запросите новое письмо и перейдите по ссылке из него.';
@override
String get verificationInvalid => 'Ссылка недействительна.';
@override
String get verificationInvalidBody =>
'Ссылка подтверждения отсутствует или недействительна. Откройте полную ссылку из письма или запросите новую.';
@override
String get verificationInternal => 'Что-то пошло не так.';
@override
String get verificationInternalBody =>
'Не удалось выполнить запрос из-за внутренней ошибки. Попробуйте ещё раз.';
@override
String get verificationResend => 'Отправить письмо ещё раз';
@override
String get verificationRequest => 'Отправить новое письмо';
@override
String get verificationRetry => 'Повторить подтверждение';
@override
String get verificationBack => 'Вернуться ко входу';
@override
String get verificationHint =>
'Нет письма? Проверьте папку «Спам» или запросите новое ниже.';
@override
String get verificationDemoLink => 'Перейти к подтверждению';
@override
String get verificationStep => 'ПОДТВЕРДИТЕ ПОЧТУ';
@override
String get verificationRecipient => 'Получатель';
@override
String get verificationChangeEmail => 'Изменить почту';
@override
String get kycTitle => 'Верификация и лимиты';
@override
String get kycIntro =>
'Ваш аккаунт — ваш темп. Подтвердите личность, когда понадобятся более высокие лимиты.';
@override
String get kycUnverified => 'Личность не подтверждена';
@override
String get kycPending => 'На проверке';
@override
String get kycApproved => 'Личность подтверждена';
@override
String get kycChanges => 'Нужны новые фото';
@override
String get kycDenied => 'Верификация отклонена';
@override
String get kycUnverifiedBody =>
'Вы можете пользоваться аккаунтом с базовыми лимитами. Для повышения отправьте фото паспорта и селфи с ним.';
@override
String get kycPendingBody =>
'Заявка ожидает проверки сотрудником. До одобрения действуют базовые лимиты. Повторная отправка не нужна.';
@override
String get kycApprovedBody =>
'Проверка завершена. Актуальные лимиты указаны ниже.';
@override
String get kycChangesBody =>
'Аккаунт доступен с базовыми лимитами. Замените фото и отправьте новую заявку.';
@override
String get kycDeniedBody =>
'Базовые лимиты остаются доступными. Если вы не согласны с решением, обратитесь в поддержку для пересмотра.';
@override
String get kycPassportReason =>
'Данные паспорта не удалось прочитать. Переснимите при хорошем освещении, чтобы все края были видны.';
@override
String get kycSelfieReason =>
'На селфи плохо видно лицо или паспорт. Оба должны быть в кадре и в фокусе.';
@override
String get kycExpiredReason =>
'Срок действия паспорта истёк. Используйте действующий паспорт и сделайте оба фото заново.';
@override
String get kycDeniedReason =>
'По этой заявке не удалось подтвердить вашу личность.';
@override
String get kycStart => 'Подтвердить личность';
@override
String get kycResubmit => 'Заменить фото';
@override
String get kycRefresh => 'Обновить статус и лимиты';
@override
String get kycSupport => 'Обратиться в поддержку';
@override
String get kycSupportBody => 'Поддержка сейчас недоступна. Попробуйте позже.';
@override
String get kycBasic => 'Базовый';
@override
String get kycVerifiedTier => 'Подтверждённый';
@override
String get kycLimits => 'Ваши лимиты';
@override
String get kycDay => 'Объём за день';
@override
String get kycMonth => 'Объём за месяц';
@override
String get kycCurrent => 'Текущий лимит';
@override
String get kycRemaining => 'Доступно';
@override
String get kycAfter => 'После одобрения';
@override
String get kycLimitsError =>
'Не удалось обновить лимиты. Значения ниже могут быть устаревшими. Повторите запрос.';
@override
String get kycLimitsEmpty =>
'Сервис не вернул лимиты. Обновите данные или уточните их в поддержке перед операцией.';
@override
String get kycLoading => 'Загружаем ваш статус…';
@override
String get kycPassport => 'Фото паспорта';
@override
String get kycSelfie => 'Селфи с паспортом';
@override
String get kycReview => 'Проверьте фото';
@override
String get kycPassportHelp =>
'Откройте действующий паспорт на странице с фото. Все четыре края и текст должны быть чётко видны. Избегайте бликов и размытия, не закрывайте данные.';
@override
String get kycSelfieHelp =>
'Держите открытый паспорт рядом с лицом. Лицо целиком и страница с фото должны быть в одном кадре, чёткими и ничем не закрытыми.';
@override
String get kycFileRules =>
'JPEG или PNG · до 10 МБ · не менее 600 × 600 пикселей. Сделайте фото камерой, затем выберите его здесь.';
@override
String get kycChoose => 'Выбрать фото';
@override
String get kycReplace => 'Выбрать другое фото';
@override
String get kycRemove => 'Удалить фото';
@override
String get kycNext => 'Продолжить';
@override
String get kycBack => 'Назад';
@override
String get kycSkip => 'Оставить базовые лимиты';
@override
String get kycClose => 'К аккаунту';
@override
String get kycReviewHelp =>
'Убедитесь, что оба фото читаемы и на них один паспорт. Их проверит сотрудник. Отправка не повышает лимиты сразу.';
@override
String get kycConfirm => 'На фото я и мой действующий паспорт.';
@override
String get kycSubmit => 'Отправить на проверку';
@override
String get kycWorking => 'Обработка… Не закрывайте страницу.';
@override
String get kycDiscardTitle => 'Удалить выбранные фото?';
@override
String get kycDiscardBody =>
'Черновик хранится только в памяти. При выходе выбранные фото будут удалены. Позже можно начать заново.';
@override
String get kycStay => 'Продолжить';
@override
String get kycDiscard => 'Удалить и выйти';
@override
String get kycErrorUnavailable =>
'Сервис недоступен. Выбранные фото сохранены в памяти. Повторите попытку.';
@override
String get kycErrorInternal =>
'Произошла ошибка. Повторите попытку; выбранные фото сохранены.';
@override
String get kycErrorPermission =>
'Не удалось открыть фото. Проверьте доступ к файлам или выберите другое изображение.';
@override
String get kycErrorFormat =>
'Выберите изображение JPEG или PNG. Другие форматы не поддерживаются.';
@override
String get kycErrorLarge =>
'Изображение слишком большое. Выберите файл до 10 МБ и 40 мегапикселей.';
@override
String get kycErrorUnreadable =>
'Изображение повреждено или не читается. Выберите другое фото.';
@override
String get kycErrorSmall =>
'Фото слишком маленькое. Ширина и высота должны быть не менее 600 пикселей.';
@override
String get kycErrorIncomplete =>
'Выберите оба фото и подтвердите, что на них вы и ваш паспорт.';
@override
String get kycErrorSession =>
'Сессия истекла. Войдите снова и проверьте статус верификации.';
@override
String get kycErrorRate =>
'Слишком много запросов. Немного подождите и повторите попытку.';
@override
String get kycErrorConflict =>
'Статус проверки изменился. Обновите его, прежде чем продолжить.';
@override
String get kycDemoControls => 'Сценарии проверки';
@override
String get kycDemoApprove => 'Одобрить';
@override
String get kycDemoCorrect => 'Запросить чёткое фото паспорта';
@override
String get kycDemoSelfie => 'Запросить чёткое селфи';
@override
String get kycDemoExpired => 'Паспорт просрочен';
@override
String get kycDemoDeny => 'Отклонить';
@override
String get kycPreview => 'Посмотреть верификацию и лимиты';
@override
String get kycChooseFirst => 'Выберите фото для продолжения.';
@override
String get kycStatusStale =>
'Не удалось обновить статус. Показан последний известный результат.';
@override
String get kycUsed => 'Использовано';
@override
String get kycReserved => 'Зарезервировано';
@override
String get kycUsageHelp =>
'Использовано — завершённые операции. Зарезервировано — суммы, удержанные для ожидающих операций. Обе суммы учитываются в лимите.';
@override
String get kycOccupied => 'Использовано + зарезервировано';
@override
String get kycNoCapacity => 'Доступный объём исчерпан';
@override
String get wAccounts => 'Счета';
@override
String get wActivity => 'История';
@override
String get wLimits => 'Лимиты';
@override
String get wSettings => 'Настройки';
@override
String get wHeadline => 'Ваши деньги.\nВаш следующий шаг.';
@override
String get wSubheading => 'Пополняйте. Обменивайте. Двигайтесь дальше.';
@override
String get wAvailable => 'Вам доступно';
@override
String get wTopUp => 'Пополнить';
@override
String get wExchange => 'Обменять';
@override
String get wPayout => 'Вывести';
@override
String get wRecent => 'Последние операции';
@override
String get wSeeAll => 'Смотреть все';
@override
String get wEmptyActivity =>
'Ваш следующий шаг начинается здесь. Здесь появятся ваши операции.';
@override
String get wEmptyAccounts =>
'Ваши счета ещё не готовы. Попробуйте обновить страницу.';
@override
String get wRefresh => 'Обновить';
@override
String get wSource => 'Со счёта';
@override
String get wDestination => 'На счёт';
@override
String get wReceiveAccount => 'Счёт для зачисления';
@override
String get wAmount => 'Сумма';
@override
String get wRecipient => 'Получатель';
@override
String get wRecipientHint => 'Имя или реквизиты получателя';
@override
String get wReview => 'Проверить';
@override
String get wReviewTitle => 'Проверьте детали.';
@override
String get wFee => 'Комиссия';
@override
String get wTotalDebit => 'Спишется со счёта';
@override
String get wCredit => 'Поступит на счёт';
@override
String get wRecipientGets => 'Получатель получит';
@override
String get wYouPay => 'Вы платите';
@override
String get wRate => 'Курс обмена';
@override
String get wQuoteNotice =>
'Эти условия действуют 60 секунд. Подтвердите их, прежде чем продолжить.';
@override
String get wExpired =>
'Срок действия расчёта истёк. Обновите его и проверьте новые условия.';
@override
String get wNewQuote => 'Обновить расчёт';
@override
String get wConfirm => 'Подтвердить операцию';
@override
String get wEdit => 'Изменить данные';
@override
String get wClose => 'Закрыть';
@override
String get wWorking => 'Обрабатываем…';
@override
String get wPending => 'В обработке';
@override
String get wCompleted => 'Завершено';
@override
String get wFailed => 'Не выполнено';
@override
String get wPendingTopUp =>
'Пополнение обрабатывается. Доступный баланс пока не изменился.';
@override
String get wPendingExchange =>
'Сумма и комиссия уже вычтены из доступного баланса. После завершения обмена деньги появятся на счёте зачисления.';
@override
String get wPendingPayout =>
'Сумма и комиссия уже вычтены из доступного баланса. Вывод ещё обрабатывается.';
@override
String get wCompleteTopUp => 'Счёт пополнен. Деньги доступны для операций.';
@override
String get wCompleteExchange =>
'Обмен завершён. Деньги доступны на счёте зачисления.';
@override
String get wCompletePayout =>
'Вывод завершён. Сумма и комиссия списаны с вашего счёта.';
@override
String get wFailedTopUp =>
'Пополнение не выполнено. На счёт ничего не зачислено.';
@override
String get wFailedOutgoing =>
'Операция не выполнена. Сумма и комиссия снова доступны на вашем счёте.';
@override
String get wUnavailable =>
'Не удалось обновить данные. Информация может быть устаревшей. Повторите попытку.';
@override
String get wInvalidAmount =>
'Введите сумму больше нуля с допустимым для валюты числом знаков после запятой.';
@override
String get wInsufficient =>
'Для этой суммы и комиссии недостаточно доступных средств. Пополните счёт или уменьшите сумму.';
@override
String get wLimitError =>
'Сумма превышает доступный лимит. Проверьте лимиты перед продолжением.';
@override
String get wRecipientError =>
'Введите тестового получателя длиной от 6 до 140 символов.';
@override
String get wUnsupported => 'Эта комбинация недоступна. Выберите другой счёт.';
@override
String get wNotFound => 'Не удалось найти запись. Вернитесь к счетам.';
@override
String get wUncertain =>
'Не удалось подтвердить результат. Проверьте его, прежде чем создавать другую операцию.';
@override
String get wCheckOutcome => 'Проверить результат';
@override
String get wNoSubmission =>
'Операция не найдена. Вы можете снова проверить и подтвердить её.';
@override
String get wDiscardTitle => 'Выйти из операции?';
@override
String get wDiscardBody =>
'Введённые данные будут удалены. Новая операция не будет отправлена.';
@override
String get wStay => 'Продолжить';
@override
String get wAll => 'Все';
@override
String get wSearch => 'Поиск по номеру';
@override
String get wNoMatches => 'Операции не найдены. Измените фильтр.';
@override
String get wClearFilters => 'Сбросить фильтры';
@override
String get wReference => 'Номер операции';
@override
String get wWhen => 'Создано';
@override
String get wViewAccount => 'Открыть счёт';
@override
String get wPreferences => 'Настройте под себя.';
@override
String get wLanguage => 'Язык аккаунта';
@override
String get wLanguageHelp => 'Используется во всём аккаунте.';
@override
String get wLanguageSaved => 'Сохранено в аккаунте.';
@override
String get wLanguageFailed =>
'Изменено для текущей сессии, но не сохранено в аккаунте. Повторите попытку ниже.';
@override
String get wProfileError =>
'Не удалось загрузить настройки аккаунта. Повторите попытку.';
@override
String get wSecurity => 'Безопасность';
@override
String get wSecurityBody =>
'Настройки пароля и безопасности пока недоступны.';
@override
String get wIdentity => 'Подтверждение личности';
@override
String get wIdentityHelp => 'Необязательная проверка для повышения лимитов.';
@override
String get wBackLogin => 'Вернуться ко входу';
@override
String get wEnter => 'Открыть свои счета';
@override
String get wUsage => 'Использовано за период';
@override
String get wLimitsScope => 'Исходящие операции · эквивалент в EUR';
@override
String get wPendingHint =>
'Обновите, чтобы узнать текущий статус. Остальными счетами можно пользоваться.';
@override
String get wFundsHeading => 'Где мои деньги?';
@override
String get wAnother => 'Создать новую операцию';
@override
String get factorTitle => 'Ещё одна проверка.';
@override
String get factorLogin => 'Подтвердите, что это вы, чтобы завершить вход.';
@override
String get factorMoney =>
'Подтвердите эту операцию. До подтверждения деньги не переводятся.';
@override
String get factorEnableHelp =>
'Подтвердите включение двухфакторной проверки для входа и операций с деньгами.';
@override
String get factorDisableHelp =>
'Подтвердите отключение двухфакторной проверки. Для входа и операций больше не потребуется второй код.';
@override
String get factorEmail => 'Введите шестизначный код из письма.';
@override
String get factorAuthenticator =>
'Введите текущий шестизначный код из приложения-аутентификатора.';
@override
String get factorCode => 'Код подтверждения';
@override
String get factorConfirm => 'Подтвердить и продолжить';
@override
String get factorResend => 'Отправить новый код';
@override
String get factorHelp =>
'Проверьте входящие и папку «Спам». Новый код заменяет предыдущий.';
@override
String get factorAppHelp =>
'Используйте текущий код. Если приложение недоступно, отмените это действие.';
@override
String get factorInvalid =>
'Неверный код. Проверьте все шесть цифр и попробуйте ещё раз.';
@override
String get factorExpired =>
'Срок проверки истёк. Запросите новый код или вернитесь и начните снова.';
@override
String get factorLocked =>
'Слишком много неверных попыток. Подождите пять минут и начните проверку снова.';
@override
String get factorWait => 'Подождите перед повторным запросом кода.';
@override
String get factorRequired =>
'Нужна повторная проверка. Подтвердите это действие ещё раз.';
@override
String get factorUnavailable =>
'Не удалось завершить проверку. Повторите попытку или отмените действие.';
@override
String factorCooldown(int seconds) {
return 'Новый код через $seconds с';
}
@override
String get factorSettings => 'Двухфакторная проверка';
@override
String get factorSettingsHelp =>
'Подтверждайте кодом вход, пополнение, обмен и вывод средств.';
@override
String get factorOn => 'Включена';
@override
String get factorOff => 'Выключена';
@override
String get factorEnable => 'Включить';
@override
String get factorDisable => 'Выключить';
@override
String get factorEmailMethod => 'Код из письма';
@override
String get factorAppMethod => 'Приложение-аутентификатор';
@override
String get factorAppExpired =>
'Срок проверки истёк. Вернитесь и начните снова.';
}
+2 -1
View File
@@ -43,10 +43,11 @@
"authCompleted": "Request completed.",
"authInvalidCredentials": "Email or password is incorrect. Please try again.",
"authEmailInUse": "This email is already registered. Try logging in.",
"authVerifyEmail": "Confirm your email before signing in.",
"authUnavailable": "The service is unavailable. Please try again.",
"authUnexpected": "Something went wrong. Please try again.",
"checkInbox": "Check your inbox.",
"verificationSent": "We’ve sent you a verification email. Open the link in your inbox to confirm your email address.",
"verificationSent": "If this email belongs to an unverified account, a verification email is on its way. Open the link in your inbox to confirm your address.",
"verificationSending": "Preparing your verification email…",
"verificationChecking": "Verifying your email…",
"verificationVerified": "Your email\nis verified.",
+2 -1
View File
@@ -43,10 +43,11 @@
"authCompleted": "Запрос выполнен.",
"authInvalidCredentials": "Неверная почта или пароль. Попробуйте ещё раз.",
"authEmailInUse": "Эта почта уже зарегистрирована. Попробуйте войти.",
"authVerifyEmail": "Подтвердите почту перед входом.",
"authUnavailable": "Сервис недоступен. Попробуйте ещё раз.",
"authUnexpected": "Что-то пошло не так. Попробуйте ещё раз.",
"checkInbox": "Проверьте почту.",
"verificationSent": "Мы отправили вам письмо для подтверждения. Откройте ссылку в письме, чтобы подтвердить электронную почту.",
"verificationSent": "Если эта почта принадлежит неподтверждённому аккаунту, письмо с подтверждением уже в пути. Откройте ссылку в письме, чтобы подтвердить адрес.",
"verificationSending": "Готовим письмо для подтверждения…",
"verificationChecking": "Подтверждаем вашу почту…",
"verificationVerified": "Ваша почта\nподтверждена.",
+22 -7
View File
@@ -20,13 +20,19 @@ import 'generated/i18n/app_localizations.dart';
Future<void> main() async {
final binding = WidgetsFlutterBinding.ensureInitialized();
final locale = await LocaleProvider.load(binding.platformDispatcher.locales);
runApp(SxchngeApp(localeProvider: locale));
const apiUrl = String.fromEnvironment('SXCHNGE_API_URL');
const clientId = String.fromEnvironment('SXCHNGE_CLIENT_ID');
final backend = apiUrl.isEmpty
? null
: BackendAuthServices(baseUri: Uri.parse(apiUrl), clientId: clientId);
runApp(SxchngeApp(localeProvider: locale, backend: backend));
}
class SxchngeApp extends StatefulWidget {
const SxchngeApp({
super.key,
this.localeProvider,
this.backend,
this.analyticsService,
this.identityService,
this.limitsService,
@@ -38,6 +44,7 @@ class SxchngeApp extends StatefulWidget {
this.verificationService = const DummyVerificationService(),
});
final LocaleProvider? localeProvider;
final BackendAuthServices? backend;
final AnalyticsService? analyticsService;
final IdentityService? identityService;
final LimitsService? limitsService;
@@ -56,7 +63,8 @@ class _SxchngeAppState extends State<SxchngeApp> {
late final _locale = widget.localeProvider ?? LocaleProvider();
late final _analytics = widget.analyticsService ?? DummyAnalyticsService();
late final _factors = widget.twoFactorService ?? DummyTwoFactorService();
late final _factors =
widget.backend ?? widget.twoFactorService ?? DummyTwoFactorService();
late final _security = SecurityProvider(_factors, _analytics);
late final _identity = widget.identityService ?? DummyIdentityService();
late final _walletService =
@@ -79,12 +87,18 @@ class _SxchngeAppState extends State<SxchngeApp> {
late final _journeys = JourneyRecorder(_analytics);
late final _services = InstrumentedAuthServices(
recorder: _journeys,
loginService: GuardedLoginService(widget.loginService, _factors),
signupService: widget.signupService,
verificationService: widget.verificationService,
loginService: GuardedLoginService(
widget.backend ?? widget.loginService,
_factors,
),
signupService: widget.backend ?? widget.signupService,
verificationService: widget.backend ?? widget.verificationService,
);
late final _router = createAppRouter()
..routeInformationProvider.addListener(_routeChanged);
late final _router = createAppRouter(
authenticated: widget.backend == null
? null
: () => widget.backend!.session != null,
)..routeInformationProvider.addListener(_routeChanged);
String _previousPath = '/';
@override
@@ -109,6 +123,7 @@ class _SxchngeAppState extends State<SxchngeApp> {
_wallet.dispose();
_security.dispose();
_locale.dispose();
widget.backend?.close();
super.dispose();
}
+12 -1
View File
@@ -19,12 +19,23 @@ import '../workspace/shell.dart';
import '../workspace/transfer.dart';
import 'routes.dart';
GoRouter createAppRouter() {
GoRouter createAppRouter({bool Function()? authenticated}) {
final topUpKey = GlobalKey<TransferPageState>();
final exchangeKey = GlobalKey<TransferPageState>();
final payoutKey = GlobalKey<TransferPageState>();
final identityKey = GlobalKey<IdentityPageState>();
return GoRouter(
redirect: (context, state) {
if (authenticated == null || authenticated()) return null;
final path = state.uri.path;
if (path == '/' ||
path == '/verify-email' ||
path == '/verification-sent' ||
path == '/two-factor') {
return null;
}
return '/';
},
// Signup email is an in-memory navigation extra, never browser-history data.
extraCodec: const _TransientExtras(),
routes: [