email_entry_page.dart 15 KB

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