email_entry_page.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. import 'dart:io';
  2. import 'package:email_validator/email_validator.dart';
  3. import 'package:flutter/cupertino.dart';
  4. import 'package:flutter/gestures.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/services.dart';
  7. import 'package:flutter/widgets.dart';
  8. import 'package:password_strength/password_strength.dart';
  9. import 'package:photos/core/configuration.dart';
  10. import 'package:photos/ente_theme_data.dart';
  11. import 'package:photos/models/billing_plan.dart';
  12. import 'package:photos/services/billing_service.dart';
  13. import 'package:photos/services/user_service.dart';
  14. import 'package:photos/ui/common/dynamicFAB.dart';
  15. import 'package:photos/ui/loading_widget.dart';
  16. import 'package:photos/ui/web_page.dart';
  17. import 'package:photos/utils/data_util.dart';
  18. import 'package:step_progress_indicator/step_progress_indicator.dart';
  19. class EmailEntryPage extends StatefulWidget {
  20. EmailEntryPage({Key key}) : super(key: key);
  21. @override
  22. _EmailEntryPageState createState() => _EmailEntryPageState();
  23. }
  24. class _EmailEntryPageState extends State<EmailEntryPage> {
  25. static const kMildPasswordStrengthThreshold = 0.4;
  26. static const kStrongPasswordStrengthThreshold = 0.7;
  27. final _config = Configuration.instance;
  28. final _passwordController1 = TextEditingController();
  29. final _passwordController2 = TextEditingController();
  30. final Color _validFieldValueColor = Color.fromRGBO(45, 194, 98, 0.2);
  31. String _email;
  32. String _password;
  33. double _passwordStrength = 0.0;
  34. bool _emailIsValid = false;
  35. bool _hasAgreedToTOS = true;
  36. bool _hasAgreedToE2E = false;
  37. bool _password1Visible = false;
  38. bool _password2Visible = false;
  39. bool _passwordsMatch = false;
  40. final _password1FocusNode = FocusNode();
  41. final _password2FocusNode = FocusNode();
  42. bool _password1InFocus = false;
  43. bool _password2InFocus = false;
  44. bool _passwordIsValid = false;
  45. @override
  46. void initState() {
  47. _email = _config.getEmail();
  48. _password1FocusNode.addListener(() {
  49. setState(() {
  50. _password1InFocus = _password1FocusNode.hasFocus;
  51. });
  52. });
  53. _password2FocusNode.addListener(() {
  54. setState(() {
  55. _password2InFocus = _password2FocusNode.hasFocus;
  56. });
  57. });
  58. super.initState();
  59. }
  60. @override
  61. Widget build(BuildContext context) {
  62. final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 125;
  63. FloatingActionButtonLocation fabLocation() {
  64. if (isKeypadOpen) {
  65. return null;
  66. } else {
  67. return FloatingActionButtonLocation.centerFloat;
  68. }
  69. }
  70. final appBar = AppBar(
  71. elevation: 0,
  72. leading: IconButton(
  73. icon: Icon(Icons.arrow_back),
  74. color: Theme.of(context).iconTheme.color,
  75. onPressed: () {
  76. Navigator.of(context).pop();
  77. },
  78. ),
  79. title: Material(
  80. type: MaterialType.transparency,
  81. child: StepProgressIndicator(
  82. totalSteps: 4,
  83. currentStep: 1,
  84. selectedColor: Theme.of(context).buttonColor,
  85. roundedEdges: Radius.circular(10),
  86. unselectedColor:
  87. Theme.of(context).colorScheme.stepProgressUnselectedColor,
  88. ),
  89. ),
  90. );
  91. return Scaffold(
  92. appBar: appBar,
  93. body: _getBody(),
  94. floatingActionButton: DynamicFAB(
  95. isKeypadOpen: isKeypadOpen,
  96. isFormValid: _isFormValid(),
  97. buttonText: 'Create account',
  98. onPressedFunction: () {
  99. _config.setVolatilePassword(_passwordController1.text);
  100. _config.setEmail(_email);
  101. UserService.instance
  102. .getOtt(context, _email, isCreateAccountScreen: true);
  103. },
  104. ),
  105. floatingActionButtonLocation: fabLocation(),
  106. floatingActionButtonAnimator: NoScalingAnimation(),
  107. );
  108. }
  109. Widget _getBody() {
  110. var passwordStrengthText = 'Weak';
  111. var passwordStrengthColor = Colors.redAccent;
  112. if (_passwordStrength > kStrongPasswordStrengthThreshold) {
  113. passwordStrengthText = 'Strong';
  114. passwordStrengthColor = Colors.greenAccent;
  115. } else if (_passwordStrength > kMildPasswordStrengthThreshold) {
  116. passwordStrengthText = 'Moderate';
  117. passwordStrengthColor = Colors.orangeAccent;
  118. }
  119. return Column(
  120. children: [
  121. Expanded(
  122. child: AutofillGroup(
  123. child: ListView(
  124. children: [
  125. Padding(
  126. padding:
  127. const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
  128. child: Text(
  129. 'Create new account',
  130. style: Theme.of(context).textTheme.headline4,
  131. ),
  132. ),
  133. Padding(
  134. padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
  135. child: TextFormField(
  136. style: Theme.of(context).textTheme.subtitle1,
  137. autofillHints: const [AutofillHints.email],
  138. decoration: InputDecoration(
  139. fillColor: _emailIsValid ? _validFieldValueColor : null,
  140. filled: true,
  141. hintText: 'Email',
  142. contentPadding:
  143. EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  144. border: UnderlineInputBorder(
  145. borderSide: BorderSide.none,
  146. borderRadius: BorderRadius.circular(6),
  147. ),
  148. suffixIcon: _emailIsValid
  149. ? Icon(
  150. Icons.check,
  151. size: 20,
  152. color: Theme.of(context)
  153. .inputDecorationTheme
  154. .focusedBorder
  155. .borderSide
  156. .color,
  157. )
  158. : null,
  159. ),
  160. onChanged: (value) {
  161. _email = value.trim();
  162. if (_emailIsValid != EmailValidator.validate(_email)) {
  163. setState(() {
  164. _emailIsValid = EmailValidator.validate(_email);
  165. });
  166. }
  167. },
  168. autocorrect: false,
  169. keyboardType: TextInputType.emailAddress,
  170. //initialValue: _email,
  171. textInputAction: TextInputAction.next,
  172. ),
  173. ),
  174. Padding(padding: EdgeInsets.all(4)),
  175. Padding(
  176. padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
  177. child: TextFormField(
  178. keyboardType: TextInputType.text,
  179. controller: _passwordController1,
  180. obscureText: !_password1Visible,
  181. enableSuggestions: true,
  182. autofillHints: const [AutofillHints.newPassword],
  183. decoration: InputDecoration(
  184. fillColor:
  185. _passwordIsValid ? _validFieldValueColor : null,
  186. filled: true,
  187. hintText: "Password",
  188. contentPadding:
  189. EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  190. suffixIcon: _password1InFocus
  191. ? IconButton(
  192. icon: Icon(
  193. _password1Visible
  194. ? Icons.visibility
  195. : Icons.visibility_off,
  196. color: Theme.of(context).iconTheme.color,
  197. size: 20,
  198. ),
  199. onPressed: () {
  200. setState(() {
  201. _password1Visible = !_password1Visible;
  202. });
  203. },
  204. )
  205. : _passwordIsValid
  206. ? Icon(
  207. Icons.check,
  208. color: Theme.of(context)
  209. .inputDecorationTheme
  210. .focusedBorder
  211. .borderSide
  212. .color,
  213. )
  214. : null,
  215. border: UnderlineInputBorder(
  216. borderSide: BorderSide.none,
  217. borderRadius: BorderRadius.circular(6),
  218. ),
  219. ),
  220. focusNode: _password1FocusNode,
  221. onChanged: (password) {
  222. if (password != _password) {
  223. setState(() {
  224. _password = password;
  225. _passwordStrength =
  226. estimatePasswordStrength(password);
  227. _passwordIsValid = _passwordStrength >=
  228. kMildPasswordStrengthThreshold;
  229. });
  230. }
  231. },
  232. onEditingComplete: () {
  233. _password1FocusNode.unfocus();
  234. _password2FocusNode.requestFocus();
  235. TextInput.finishAutofillContext();
  236. },
  237. ),
  238. ),
  239. const SizedBox(height: 8),
  240. Padding(
  241. padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
  242. child: TextFormField(
  243. keyboardType: TextInputType.visiblePassword,
  244. controller: _passwordController2,
  245. obscureText: !_password2Visible,
  246. autofillHints: const [AutofillHints.newPassword],
  247. onEditingComplete: () => TextInput.finishAutofillContext(),
  248. decoration: InputDecoration(
  249. fillColor: _passwordsMatch ? _validFieldValueColor : null,
  250. filled: true,
  251. hintText: "Confirm password",
  252. contentPadding:
  253. EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  254. suffixIcon: _password2InFocus
  255. ? IconButton(
  256. icon: Icon(
  257. _password2Visible
  258. ? Icons.visibility
  259. : Icons.visibility_off,
  260. color: Theme.of(context).iconTheme.color,
  261. size: 20,
  262. ),
  263. onPressed: () {
  264. setState(() {
  265. _password2Visible = !_password2Visible;
  266. });
  267. },
  268. )
  269. : _passwordsMatch
  270. ? Icon(
  271. Icons.check,
  272. color: Theme.of(context)
  273. .inputDecorationTheme
  274. .focusedBorder
  275. .borderSide
  276. .color,
  277. )
  278. : null,
  279. border: UnderlineInputBorder(
  280. borderSide: BorderSide.none,
  281. borderRadius: BorderRadius.circular(6),
  282. ),
  283. ),
  284. focusNode: _password2FocusNode,
  285. onChanged: (cnfPassword) {
  286. setState(() {
  287. if (_password != null || _password != '') {
  288. _passwordsMatch = _password == cnfPassword;
  289. }
  290. });
  291. },
  292. ),
  293. ),
  294. Opacity(
  295. opacity: (_password != '') && _password1InFocus ? 1 : 0,
  296. child: Padding(
  297. padding:
  298. const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
  299. child: Text(
  300. 'Password strength: $passwordStrengthText',
  301. style: TextStyle(
  302. color: passwordStrengthColor,
  303. fontWeight: FontWeight.w500,
  304. fontSize: 12,
  305. ),
  306. ),
  307. ),
  308. ),
  309. const SizedBox(height: 4),
  310. const Divider(thickness: 1),
  311. const SizedBox(height: 12),
  312. _getAgreement(),
  313. Padding(padding: EdgeInsets.all(20)),
  314. ],
  315. ),
  316. ),
  317. ),
  318. ],
  319. );
  320. }
  321. Container _getAgreement() {
  322. return Container(
  323. padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
  324. child: Column(
  325. children: [
  326. _getTOSAgreement(),
  327. _getPasswordAgreement(),
  328. ],
  329. ),
  330. );
  331. }
  332. Widget _getTOSAgreement() {
  333. return GestureDetector(
  334. onTap: () {
  335. setState(() {
  336. _hasAgreedToTOS = !_hasAgreedToTOS;
  337. });
  338. },
  339. behavior: HitTestBehavior.translucent,
  340. child: Row(
  341. children: [
  342. Checkbox(
  343. value: _hasAgreedToTOS,
  344. side: CheckboxTheme.of(context).side,
  345. onChanged: (value) {
  346. setState(() {
  347. _hasAgreedToTOS = value;
  348. });
  349. },
  350. ),
  351. Expanded(
  352. child: RichText(
  353. text: TextSpan(
  354. children: [
  355. TextSpan(
  356. text: "I agree to the ",
  357. ),
  358. TextSpan(
  359. text: "terms of service",
  360. style: TextStyle(
  361. decoration: TextDecoration.underline,
  362. ),
  363. recognizer: TapGestureRecognizer()
  364. ..onTap = () {
  365. Navigator.of(context).push(
  366. MaterialPageRoute(
  367. builder: (BuildContext context) {
  368. return WebPage("Terms", "https://ente.io/terms");
  369. },
  370. ),
  371. );
  372. },
  373. ),
  374. TextSpan(text: " and "),
  375. TextSpan(
  376. text: "privacy policy",
  377. style: TextStyle(
  378. decoration: TextDecoration.underline,
  379. ),
  380. recognizer: TapGestureRecognizer()
  381. ..onTap = () {
  382. Navigator.of(context).push(
  383. MaterialPageRoute(
  384. builder: (BuildContext context) {
  385. return WebPage(
  386. "Privacy",
  387. "https://ente.io/privacy",
  388. );
  389. },
  390. ),
  391. );
  392. },
  393. ),
  394. ],
  395. style: Theme.of(context)
  396. .textTheme
  397. .subtitle1
  398. .copyWith(fontSize: 12),
  399. ),
  400. textAlign: TextAlign.left,
  401. ),
  402. ),
  403. ],
  404. ),
  405. );
  406. }
  407. Widget _getPasswordAgreement() {
  408. return GestureDetector(
  409. onTap: () {
  410. setState(() {
  411. _hasAgreedToE2E = !_hasAgreedToE2E;
  412. });
  413. },
  414. behavior: HitTestBehavior.translucent,
  415. child: Row(
  416. children: [
  417. Checkbox(
  418. value: _hasAgreedToE2E,
  419. side: CheckboxTheme.of(context).side,
  420. onChanged: (value) {
  421. setState(() {
  422. _hasAgreedToE2E = value;
  423. });
  424. },
  425. ),
  426. Expanded(
  427. child: RichText(
  428. text: TextSpan(
  429. children: [
  430. TextSpan(
  431. text:
  432. "I understand that if I lose my password, I may lose my data since my data is ",
  433. ),
  434. TextSpan(
  435. text: "end-to-end encrypted",
  436. style: TextStyle(
  437. decoration: TextDecoration.underline,
  438. ),
  439. recognizer: TapGestureRecognizer()
  440. ..onTap = () {
  441. Navigator.of(context).push(
  442. MaterialPageRoute(
  443. builder: (BuildContext context) {
  444. return WebPage(
  445. "encryption",
  446. "https://ente.io/architecture",
  447. );
  448. },
  449. ),
  450. );
  451. },
  452. ),
  453. TextSpan(text: " with ente"),
  454. ],
  455. style: Theme.of(context)
  456. .textTheme
  457. .subtitle1
  458. .copyWith(fontSize: 12),
  459. ),
  460. textAlign: TextAlign.left,
  461. ),
  462. ),
  463. ],
  464. ),
  465. );
  466. }
  467. bool _isFormValid() {
  468. return _emailIsValid &&
  469. _passwordsMatch &&
  470. _hasAgreedToTOS &&
  471. _hasAgreedToE2E;
  472. }
  473. }
  474. class PricingWidget extends StatelessWidget {
  475. const PricingWidget({
  476. Key key,
  477. }) : super(key: key);
  478. @override
  479. Widget build(BuildContext context) {
  480. return FutureBuilder<BillingPlans>(
  481. future: BillingService.instance.getBillingPlans(),
  482. builder: (BuildContext context, AsyncSnapshot snapshot) {
  483. if (snapshot.hasData) {
  484. return _buildPlans(context, snapshot.data);
  485. } else if (snapshot.hasError) {
  486. return Text("Oops, Something went wrong.");
  487. }
  488. return loadWidget;
  489. },
  490. );
  491. }
  492. Container _buildPlans(BuildContext context, BillingPlans plans) {
  493. final planWidgets = <BillingPlanWidget>[];
  494. for (final plan in plans.plans) {
  495. final productID = Platform.isAndroid ? plan.androidID : plan.iosID;
  496. if (productID != null && productID.isNotEmpty) {
  497. planWidgets.add(BillingPlanWidget(plan));
  498. }
  499. }
  500. final freePlan = plans.freePlan;
  501. return Container(
  502. height: 280,
  503. color: Theme.of(context).cardColor,
  504. child: Column(
  505. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  506. children: <Widget>[
  507. Text(
  508. "pricing",
  509. style: TextStyle(
  510. fontWeight: FontWeight.bold,
  511. fontSize: 18,
  512. ),
  513. ),
  514. SingleChildScrollView(
  515. scrollDirection: Axis.horizontal,
  516. child: Row(
  517. mainAxisAlignment: MainAxisAlignment.center,
  518. children: planWidgets,
  519. ),
  520. ),
  521. Text(
  522. "We offer a free trial of " +
  523. convertBytesToReadableFormat(freePlan.storage) +
  524. " for " +
  525. freePlan.duration.toString() +
  526. " " +
  527. freePlan.period,
  528. ),
  529. GestureDetector(
  530. child: Row(
  531. mainAxisAlignment: MainAxisAlignment.center,
  532. crossAxisAlignment: CrossAxisAlignment.center,
  533. children: const [
  534. Icon(
  535. Icons.close,
  536. size: 12,
  537. color: Colors.white38,
  538. ),
  539. Padding(padding: EdgeInsets.all(1)),
  540. Text(
  541. "close",
  542. style: TextStyle(
  543. color: Colors.white38,
  544. ),
  545. ),
  546. ],
  547. ),
  548. onTap: () => Navigator.pop(context),
  549. )
  550. ],
  551. ),
  552. );
  553. }
  554. }
  555. class BillingPlanWidget extends StatelessWidget {
  556. final BillingPlan plan;
  557. const BillingPlanWidget(
  558. this.plan, {
  559. Key key,
  560. }) : super(key: key);
  561. @override
  562. Widget build(BuildContext context) {
  563. return Padding(
  564. padding: const EdgeInsets.all(2.0),
  565. child: Card(
  566. shape: RoundedRectangleBorder(
  567. borderRadius: BorderRadius.circular(12.0),
  568. ),
  569. color: Colors.black.withOpacity(0.2),
  570. child: Container(
  571. padding: EdgeInsets.fromLTRB(12, 20, 12, 20),
  572. child: Column(
  573. children: [
  574. Text(
  575. convertBytesToGBs(plan.storage, precision: 0).toString() +
  576. " GB",
  577. style: TextStyle(
  578. fontWeight: FontWeight.bold,
  579. fontSize: 16,
  580. ),
  581. ),
  582. Padding(
  583. padding: EdgeInsets.all(4),
  584. ),
  585. Text(
  586. plan.price + " / " + plan.period,
  587. style: TextStyle(
  588. fontSize: 12,
  589. color: Colors.white70,
  590. ),
  591. ),
  592. ],
  593. ),
  594. ),
  595. ),
  596. );
  597. }
  598. }