email_entry_page.dart 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. import 'dart:io';
  2. import 'package:flutter/cupertino.dart';
  3. import 'package:flutter/gestures.dart';
  4. import 'package:flutter/material.dart';
  5. import 'package:flutter/widgets.dart';
  6. import 'package:flutter_gen/gen_l10n/app_localizations.dart';
  7. import 'package:flutter_password_strength/flutter_password_strength.dart';
  8. import 'package:photos/core/configuration.dart';
  9. import 'package:photos/models/billing_plan.dart';
  10. import 'package:photos/services/billing_service.dart';
  11. import 'package:photos/services/user_service.dart';
  12. import 'package:photos/ui/common_elements.dart';
  13. import 'package:photos/ui/loading_widget.dart';
  14. import 'package:photos/ui/web_page.dart';
  15. import 'package:photos/utils/data_util.dart';
  16. import 'package:photos/utils/dialog_util.dart';
  17. import 'package:photos/utils/email_util.dart';
  18. class EmailEntryPage extends StatefulWidget {
  19. EmailEntryPage({Key key}) : super(key: key);
  20. @override
  21. _EmailEntryPageState createState() => _EmailEntryPageState();
  22. }
  23. class _EmailEntryPageState extends State<EmailEntryPage> {
  24. static const kPasswordStrengthThreshold = 0.4;
  25. final _config = Configuration.instance;
  26. final _passwordController1 = TextEditingController(),
  27. _passwordController2 = TextEditingController();
  28. String _email;
  29. double _passwordStrength = 0;
  30. bool _hasAgreedToTOS = true;
  31. bool _hasAgreedToE2E = false;
  32. bool _password1Visible = false;
  33. bool _password2Visible = false;
  34. final _password1FocusNode = FocusNode();
  35. final _password2FocusNode = FocusNode();
  36. bool _password1InFocus = false;
  37. bool _password2InFocus = false;
  38. @override
  39. void initState() {
  40. _email = _config.getEmail();
  41. _password1FocusNode.addListener(() {
  42. setState(() {
  43. _password1InFocus = _password1FocusNode.hasFocus;
  44. });
  45. });
  46. _password2FocusNode.addListener(() {
  47. setState(() {
  48. _password2InFocus = _password2FocusNode.hasFocus;
  49. });
  50. });
  51. super.initState();
  52. }
  53. @override
  54. Widget build(BuildContext context) {
  55. final appBar = AppBar(
  56. title: Hero(
  57. tag: "sign_up",
  58. child: Material(
  59. type: MaterialType.transparency,
  60. child: Text(
  61. AppLocalizations.of(context).sign_up,
  62. style: TextStyle(
  63. fontSize: 18,
  64. letterSpacing: 0.6,
  65. ),
  66. ),
  67. ),
  68. ),
  69. );
  70. return Scaffold(
  71. appBar: appBar,
  72. body: _getBody(),
  73. // resizeToAvoidBottomInset: false,
  74. );
  75. }
  76. Widget _getBody() {
  77. return Column(
  78. children: [
  79. FlutterPasswordStrength(
  80. password: _passwordController1.text,
  81. backgroundColor: Colors.white.withOpacity(0.1),
  82. strengthCallback: (strength) {
  83. _passwordStrength = strength;
  84. },
  85. strengthColors: passwordStrengthColors,
  86. ),
  87. Expanded(
  88. child: ListView(
  89. children: [
  90. Padding(padding: EdgeInsets.all(40)),
  91. Padding(
  92. padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
  93. child: TextFormField(
  94. decoration: InputDecoration(
  95. hintText: 'email',
  96. hintStyle: TextStyle(
  97. color: Colors.white30,
  98. ),
  99. contentPadding: EdgeInsets.all(12),
  100. ),
  101. onChanged: (value) {
  102. setState(() {
  103. _email = value.trim();
  104. });
  105. },
  106. autocorrect: false,
  107. keyboardType: TextInputType.emailAddress,
  108. initialValue: _email,
  109. textInputAction: TextInputAction.next,
  110. ),
  111. ),
  112. Padding(padding: EdgeInsets.all(8)),
  113. Padding(
  114. padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
  115. child: TextFormField(
  116. keyboardType: TextInputType.text,
  117. controller: _passwordController1,
  118. obscureText: !_password1Visible,
  119. decoration: InputDecoration(
  120. hintText: "password",
  121. hintStyle: TextStyle(
  122. color: Colors.white30,
  123. ),
  124. contentPadding: EdgeInsets.all(12),
  125. suffixIcon: _password1InFocus
  126. ? IconButton(
  127. icon: Icon(
  128. _password1Visible
  129. ? Icons.visibility
  130. : Icons.visibility_off,
  131. color: Colors.white.withOpacity(0.5),
  132. size: 20,
  133. ),
  134. onPressed: () {
  135. setState(() {
  136. _password1Visible = !_password1Visible;
  137. });
  138. },
  139. )
  140. : null,
  141. ),
  142. focusNode: _password1FocusNode,
  143. onChanged: (_) {
  144. setState(() {});
  145. },
  146. onEditingComplete: () {
  147. _password1FocusNode.unfocus();
  148. _password2FocusNode.requestFocus();
  149. },
  150. ),
  151. ),
  152. Padding(padding: EdgeInsets.all(8)),
  153. Padding(
  154. padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
  155. child: TextFormField(
  156. keyboardType: TextInputType.text,
  157. controller: _passwordController2,
  158. obscureText: !_password2Visible,
  159. decoration: InputDecoration(
  160. hintText: "confirm password",
  161. hintStyle: TextStyle(
  162. color: Colors.white30,
  163. ),
  164. contentPadding: EdgeInsets.all(12),
  165. suffixIcon: _password2InFocus
  166. ? IconButton(
  167. icon: Icon(
  168. _password2Visible
  169. ? Icons.visibility
  170. : Icons.visibility_off,
  171. color: Colors.white.withOpacity(0.5),
  172. size: 20,
  173. ),
  174. onPressed: () {
  175. setState(() {
  176. _password2Visible = !_password2Visible;
  177. });
  178. },
  179. )
  180. : null,
  181. ),
  182. focusNode: _password2FocusNode,
  183. ),
  184. ),
  185. Padding(
  186. padding: EdgeInsets.all(20),
  187. ),
  188. _getAgreement(),
  189. Padding(padding: EdgeInsets.all(20)),
  190. Container(
  191. width: double.infinity,
  192. height: 64,
  193. padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
  194. child: button(
  195. AppLocalizations.of(context).sign_up,
  196. onPressed: _isFormValid()
  197. ? () {
  198. if (!isValidEmail(_email)) {
  199. showErrorDialog(context, "invalid email",
  200. "please enter a valid email address.");
  201. } else if (_passwordController1.text !=
  202. _passwordController2.text) {
  203. showErrorDialog(context, "uhm...",
  204. "the passwords you entered don't match");
  205. } else if (_passwordStrength <
  206. kPasswordStrengthThreshold) {
  207. showErrorDialog(context, "weak password",
  208. "the password you have chosen is too simple, please choose another one");
  209. } else {
  210. _config
  211. .setVolatilePassword(_passwordController1.text);
  212. _config.setEmail(_email);
  213. UserService.instance.getOtt(context, _email);
  214. }
  215. }
  216. : null,
  217. fontSize: 18,
  218. ),
  219. ),
  220. ],
  221. ),
  222. ),
  223. ],
  224. );
  225. }
  226. Container _getAgreement() {
  227. return Container(
  228. padding: const EdgeInsets.only(left: 20, right: 20),
  229. child: Column(
  230. children: [
  231. _getTOSAgreement(),
  232. _getPasswordAgreement(),
  233. ],
  234. ),
  235. );
  236. }
  237. Widget _getTOSAgreement() {
  238. return GestureDetector(
  239. onTap: () {
  240. setState(() {
  241. _hasAgreedToTOS = !_hasAgreedToTOS;
  242. });
  243. },
  244. behavior: HitTestBehavior.translucent,
  245. child: Row(
  246. children: [
  247. Checkbox(
  248. value: _hasAgreedToTOS,
  249. onChanged: (value) {
  250. setState(() {
  251. _hasAgreedToTOS = value;
  252. });
  253. }),
  254. Expanded(
  255. child: RichText(
  256. text: TextSpan(
  257. children: [
  258. TextSpan(
  259. text: "I agree to the ",
  260. ),
  261. TextSpan(
  262. text: "terms of service",
  263. style: TextStyle(
  264. color: Colors.blue,
  265. fontFamily: 'Ubuntu',
  266. ),
  267. recognizer: TapGestureRecognizer()
  268. ..onTap = () {
  269. Navigator.of(context).push(
  270. MaterialPageRoute(
  271. builder: (BuildContext context) {
  272. return WebPage("terms", "https://ente.io/terms");
  273. },
  274. ),
  275. );
  276. },
  277. ),
  278. TextSpan(text: " and "),
  279. TextSpan(
  280. text: "privacy policy",
  281. style: TextStyle(
  282. color: Colors.blue,
  283. fontFamily: 'Ubuntu',
  284. ),
  285. recognizer: TapGestureRecognizer()
  286. ..onTap = () {
  287. Navigator.of(context).push(
  288. MaterialPageRoute(
  289. builder: (BuildContext context) {
  290. return WebPage(
  291. "privacy", "https://ente.io/privacy");
  292. },
  293. ),
  294. );
  295. },
  296. ),
  297. ],
  298. style: TextStyle(
  299. height: 1.25,
  300. fontSize: 12,
  301. fontFamily: 'Ubuntu',
  302. color: Colors.white70,
  303. ),
  304. ),
  305. textAlign: TextAlign.left,
  306. ),
  307. ),
  308. ],
  309. ),
  310. );
  311. }
  312. Widget _getPasswordAgreement() {
  313. return GestureDetector(
  314. onTap: () {
  315. setState(() {
  316. _hasAgreedToE2E = !_hasAgreedToE2E;
  317. });
  318. },
  319. behavior: HitTestBehavior.translucent,
  320. child: Row(
  321. children: [
  322. Checkbox(
  323. value: _hasAgreedToE2E,
  324. onChanged: (value) {
  325. setState(() {
  326. _hasAgreedToE2E = value;
  327. });
  328. }),
  329. Expanded(
  330. child: RichText(
  331. text: TextSpan(
  332. children: [
  333. TextSpan(
  334. text:
  335. "I understand that if I lose my password, I may lose my data since my data is ",
  336. ),
  337. TextSpan(
  338. text: "end-to-end encrypted",
  339. style: TextStyle(
  340. color: Colors.blue,
  341. fontFamily: 'Ubuntu',
  342. ),
  343. recognizer: TapGestureRecognizer()
  344. ..onTap = () {
  345. Navigator.of(context).push(
  346. MaterialPageRoute(
  347. builder: (BuildContext context) {
  348. return WebPage(
  349. "encryption", "https://ente.io/architecture");
  350. },
  351. ),
  352. );
  353. },
  354. ),
  355. TextSpan(text: " with ente"),
  356. ],
  357. style: TextStyle(
  358. height: 1.5,
  359. fontSize: 12,
  360. fontFamily: 'Ubuntu',
  361. color: Colors.white70,
  362. ),
  363. ),
  364. textAlign: TextAlign.left,
  365. ),
  366. ),
  367. ],
  368. ),
  369. );
  370. }
  371. bool _isFormValid() {
  372. return _email != null &&
  373. _email.isNotEmpty &&
  374. _passwordController1.text.isNotEmpty &&
  375. _passwordController2.text.isNotEmpty &&
  376. _hasAgreedToTOS &&
  377. _hasAgreedToE2E;
  378. }
  379. }
  380. class PricingWidget extends StatelessWidget {
  381. const PricingWidget({
  382. Key key,
  383. }) : super(key: key);
  384. @override
  385. Widget build(BuildContext context) {
  386. return FutureBuilder<BillingPlans>(
  387. future: BillingService.instance.getBillingPlans(),
  388. builder: (BuildContext context, AsyncSnapshot snapshot) {
  389. if (snapshot.hasData) {
  390. return _buildPlans(context, snapshot.data);
  391. } else if (snapshot.hasError) {
  392. return Text("Oops, something went wrong.");
  393. }
  394. return loadWidget;
  395. },
  396. );
  397. }
  398. Container _buildPlans(BuildContext context, BillingPlans plans) {
  399. final planWidgets = <BillingPlanWidget>[];
  400. for (final plan in plans.plans) {
  401. final productID = Platform.isAndroid ? plan.androidID : plan.iosID;
  402. if (productID != null && productID.isNotEmpty) {
  403. planWidgets.add(BillingPlanWidget(plan));
  404. }
  405. }
  406. final freePlan = plans.freePlan;
  407. return Container(
  408. height: 280,
  409. color: Theme.of(context).cardColor,
  410. child: Column(
  411. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  412. children: <Widget>[
  413. Text(
  414. "pricing",
  415. style: TextStyle(
  416. fontWeight: FontWeight.bold,
  417. fontSize: 18,
  418. ),
  419. ),
  420. SingleChildScrollView(
  421. scrollDirection: Axis.horizontal,
  422. child: Row(
  423. mainAxisAlignment: MainAxisAlignment.center,
  424. children: planWidgets,
  425. ),
  426. ),
  427. Text("we offer a free trial of " +
  428. convertBytesToReadableFormat(freePlan.storage) +
  429. " for " +
  430. freePlan.duration.toString() +
  431. " " +
  432. freePlan.period),
  433. GestureDetector(
  434. child: Row(
  435. mainAxisAlignment: MainAxisAlignment.center,
  436. crossAxisAlignment: CrossAxisAlignment.center,
  437. children: const [
  438. Icon(
  439. Icons.close,
  440. size: 12,
  441. color: Colors.white38,
  442. ),
  443. Padding(padding: EdgeInsets.all(1)),
  444. Text(
  445. "close",
  446. style: TextStyle(
  447. color: Colors.white38,
  448. ),
  449. ),
  450. ],
  451. ),
  452. onTap: () => Navigator.pop(context),
  453. )
  454. ],
  455. ),
  456. );
  457. }
  458. }
  459. class BillingPlanWidget extends StatelessWidget {
  460. final BillingPlan plan;
  461. const BillingPlanWidget(
  462. this.plan, {
  463. Key key,
  464. }) : super(key: key);
  465. @override
  466. Widget build(BuildContext context) {
  467. return Padding(
  468. padding: const EdgeInsets.all(2.0),
  469. child: Card(
  470. shape: RoundedRectangleBorder(
  471. borderRadius: BorderRadius.circular(12.0),
  472. ),
  473. color: Colors.black.withOpacity(0.2),
  474. child: Container(
  475. padding: EdgeInsets.fromLTRB(12, 20, 12, 20),
  476. child: Column(
  477. children: [
  478. Text(
  479. convertBytesToGBs(plan.storage, precision: 0).toString() +
  480. " GB",
  481. style: TextStyle(
  482. fontWeight: FontWeight.bold,
  483. fontSize: 16,
  484. ),
  485. ),
  486. Padding(
  487. padding: EdgeInsets.all(4),
  488. ),
  489. Text(
  490. plan.price + " / " + plan.period,
  491. style: TextStyle(
  492. fontSize: 12,
  493. color: Colors.white70,
  494. ),
  495. ),
  496. ],
  497. ),
  498. ),
  499. ),
  500. );
  501. }
  502. }