Skip to content
This repository has been archived by the owner on Sep 17, 2024. It is now read-only.

Commit

Permalink
Merge pull request #81 from MXCzkEVM/add_token_handler
Browse files Browse the repository at this point in the history
fix: Passcode UI/UX audit & Added animations
  • Loading branch information
reasje authored Oct 5, 2023
2 parents b3c1e42 + 867844c commit 6196037
Show file tree
Hide file tree
Showing 10 changed files with 274 additions and 36 deletions.
2 changes: 1 addition & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<data android:scheme="mailto" />
</intent>
</queries>

<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:developer';

import 'package:datadashwallet/app/app.dart';
import 'package:datadashwallet/features/security/presentation/passcode_base/widget/numbers_row_widget.dart';
import 'package:datadashwallet/features/security/presentation/passcode_require/widgets/circle_animation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_svg/svg.dart';
Expand Down Expand Up @@ -32,24 +34,10 @@ abstract class PasscodeBasePage extends HookConsumerWidget {

ProviderBase<PasscodeBasePageState> get state;

Widget numbersRow(BuildContext context, WidgetRef ref) => Row(
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < ref.watch(state).expectedNumbersLength; i++) ...[
SvgPicture.asset(
'assets/svg/security/ic_ring.svg',
height: 32,
width: 32,
colorFilter: filterFor(
ref.watch(state).enteredNumbers.length > i
? ColorsTheme.of(context).primary60
: ColorsTheme.of(context).iconWhite,
),
),
if (i != ref.watch(state).expectedNumbersLength - 1)
const SizedBox(width: 16),
],
],
Widget numbersRow(BuildContext context, WidgetRef ref) => NumbersRowWidget(
expectedNumbersLength: ref.watch(state).expectedNumbersLength,
enteredNumbers: ref.watch(state).enteredNumbers.length,
shakeAnimationInt: ref.read(presenter).initShakeAnimationController,
);

Widget numpad(
Expand Down Expand Up @@ -191,8 +179,11 @@ abstract class PasscodeBasePage extends HookConsumerWidget {
style: FontTheme.of(context).body1.white(),
),
const SizedBox(height: 64),
Center(
child: numbersRow(context, ref),
SizedBox(
height: 57.5,
child: Center(
child: numbersRow(context, ref),
),
),
],
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,21 @@ abstract class PasscodeBasePagePresenter<T extends PasscodeBasePageState>
}

void onAddNumber(int number) async {
state.errorText = null;
state.enteredNumbers.add(number);
notify();
if (state.enteredNumbers.length != state.expectedNumbersLength) return;
state.errorText = null;
onAllNumbersEntered(state.dismissedPage);
}

void onRemoveNumber() {
if (state.enteredNumbers.isEmpty) return;
notify(() => state.enteredNumbers.removeLast());
}

void initShakeAnimationController(AnimationController animationController) {
state.shakeAnimationController = animationController;
}

void startShakeAnimation() {}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';

class PasscodeBasePageState with EquatableMixin {
final int expectedNumbersLength = 6;
List<int> enteredNumbers = [];
String? errorText;
bool isBiometricEnabled = false;
bool userHasActiveFingerprints = false;
AnimationController? shakeAnimationController;

String? dismissedPage;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import 'dart:math';

import 'package:datadashwallet/common/color_filter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mxc_ui/mxc_ui.dart';

import '../../passcode_require/widgets/circle_animation.dart';

class NumbersRowWidget extends StatefulWidget {
const NumbersRowWidget(
{super.key,
required this.expectedNumbersLength,
required this.enteredNumbers,
required this.shakeAnimationInt});

final int expectedNumbersLength;
final int enteredNumbers;
final void Function(AnimationController) shakeAnimationInt;

@override
State<NumbersRowWidget> createState() => _NumbersRowWidgetState();
}

class _NumbersRowWidgetState extends State<NumbersRowWidget>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _animationRotation;
int shakeOffset = 10;
int shakeCount = 4;

@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);

final tweenSequenceList = [
TweenSequenceItem<double>(
tween: Tween(begin: 0, end: 2 * pi / 360),
weight: 1,
),
for (int i = 0; i < 6; i++)
TweenSequenceItem<double>(
tween: Tween(begin: 2 * pi / 360, end: -2 * pi / 360)
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 1,
),
TweenSequenceItem<double>(
tween: Tween(begin: 0, end: 0),
weight: 1,
),
];

_animationRotation = TweenSequence<double>(tweenSequenceList).animate(
CurvedAnimation(parent: _animationController, curve: Curves.linear),
);

_animationController.addStatusListener(_updateStatus);

widget.shakeAnimationInt(_animationController);
}

@override
void dispose() {
_animationController.removeStatusListener(_updateStatus);
_animationController.dispose();
super.dispose();
}

void _updateStatus(AnimationStatus status) {
if (status == AnimationStatus.completed) {
_animationController.reset();
}
}

@override
Widget build(BuildContext context) {
return Expanded(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.rotate(
angle: _animationRotation.value,
child: child,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (var i = 0; i < widget.expectedNumbersLength; i++) ...[
widget.enteredNumbers > i
? const Expanded(child: CircleAnimation())
: Expanded(
child: SvgPicture.asset(
'assets/svg/security/ic_ring.svg',
height: 32,
width: 32,
colorFilter: filterFor(
ColorsTheme.of(context).iconWhite,
),
),
),
// if (i != ref.watch(state).expectedNumbersLength - 1)
// const SizedBox(width: 16),
],
],
),
),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,24 +68,33 @@ class PasscodeRequirePage extends PasscodeBasePage {
style: FontTheme.of(context).body1.textWhite(),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
Center(
child: numbersRow(context, ref),
),
SizedBox(
height: 84,
child: Column(
children: [
Expanded(child: Container()),
Expanded(
flex: 2,
child: Center(
child: numbersRow(context, ref),
),
)
],
)),
],
),
),
const SizedBox(height: 12),
buildErrorMessage(context, ref),
Padding(
padding: const EdgeInsets.only(top: 40, left: 24, right: 24),
child: MxcButton.secondaryWhite(
key: const ValueKey('forgotPasscodeButton'),
title: FlutterI18n.translate(context, 'forgot_passcode'),
size: AxsButtonSize.xl,
onTap: () => showResetPasscodeDialog(context, ref),
if (ref.watch(state).wrongInputCounter != 0)
Padding(
padding: const EdgeInsets.only(top: 40, left: 24, right: 24),
child: MxcButton.secondaryWhite(
key: const ValueKey('forgotPasscodeButton'),
title: FlutterI18n.translate(context, 'forgot_passcode'),
size: AxsButtonSize.xl,
onTap: () => showResetPasscodeDialog(context, ref),
),
),
),
const Spacer(),
numpad(context, ref),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:datadashwallet/core/core.dart';
import 'package:datadashwallet/features/security/security.dart';
import 'package:datadashwallet/features/splash/splash.dart';
import 'package:vibration/vibration.dart';

import 'passcode_require_state.dart';
import 'wrapper/passcode_require_wrapper_presenter.dart';
Expand All @@ -27,13 +28,22 @@ class PasscodeRequirePresenter
});
}

@override
void startShakeAnimation() {
if (state.shakeAnimationController != null) {
state.shakeAnimationController!.forward();
}
}

@override
void onAllNumbersEntered(String? dismissedPage) async {
if (state.enteredNumbers.join('') != _passcodeUseCase.passcode.value) {
if (state.wrongInputCounter < 6) {
if (state.wrongInputCounter < 5) {
state.wrongInputCounter++;
state.errorText = translate('attempts_x')!
.replaceFirst('{0}', '${6 - state.wrongInputCounter}');
vibrate();
startShakeAnimation();
} else {
state.errorText = null;
state.wrongInputCounter = 0;
Expand Down Expand Up @@ -64,4 +74,10 @@ class PasscodeRequirePresenter

return result;
}

void vibrate() async {
if (await Vibration.hasVibrator() ?? false) {
Vibration.vibrate(duration: 400);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:mxc_ui/mxc_ui.dart';

class CircleAnimation extends StatefulWidget {
const CircleAnimation({super.key});

@override
State<CircleAnimation> createState() => _CircleAnimationState();
}

class _CircleAnimationState extends State<CircleAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animationSize;
late Animation<double> _animationOpacity;

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_animationSize = Tween<double>(begin: 57.5, end: 32.0).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
),
);
_animationOpacity = Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
),
);
_controller.forward();
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return SizedBox(
height: 57.5,
child: Stack(
fit: StackFit.passthrough,
children: [
Center(
child: Container(
height: 32,
width: 32,
decoration: BoxDecoration(
border: Border.all(
color: ColorsTheme.of(context).iconWhite, width: 2),
shape: BoxShape.circle,
),
),
),
AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return Center(
child: Opacity(
opacity: _animationOpacity.value,
child: Container(
height: _animationSize.value,
width: _animationSize.value,
decoration: BoxDecoration(
color: ColorsTheme.of(context).iconWhite,
shape: BoxShape.circle,
),
),
),
);
},
// child: ,
),
],
),
);
}
}
Loading

0 comments on commit 6196037

Please sign in to comment.