settings_page.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. import 'dart:io';
  2. import 'package:archive/archive_io.dart';
  3. import 'package:crisp/crisp.dart';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/widgets.dart';
  7. import 'package:flutter_email_sender/flutter_email_sender.dart';
  8. import 'package:flutter_sodium/flutter_sodium.dart';
  9. import 'package:flutter_windowmanager/flutter_windowmanager.dart';
  10. import 'package:package_info_plus/package_info_plus.dart';
  11. import 'package:path_provider/path_provider.dart';
  12. import 'package:photos/core/constants.dart';
  13. import 'package:photos/ui/app_lock.dart';
  14. import 'package:photos/utils/auth_util.dart';
  15. import 'package:photos/core/configuration.dart';
  16. import 'package:photos/db/files_db.dart';
  17. import 'package:photos/services/billing_service.dart';
  18. import 'package:photos/ui/loading_widget.dart';
  19. import 'package:photos/ui/subscription_page.dart';
  20. import 'package:photos/ui/web_page.dart';
  21. import 'package:photos/utils/data_util.dart';
  22. import 'package:photos/utils/dialog_util.dart';
  23. import 'package:photos/utils/toast_util.dart';
  24. import 'package:share/share.dart';
  25. import 'package:url_launcher/url_launcher.dart';
  26. class SettingsPage extends StatelessWidget {
  27. const SettingsPage({Key key}) : super(key: key);
  28. @override
  29. Widget build(BuildContext context) {
  30. return Scaffold(
  31. appBar: AppBar(
  32. title: Text("settings"),
  33. ),
  34. body: _getBody(),
  35. );
  36. }
  37. Widget _getBody() {
  38. final hasLoggedIn = Configuration.instance.getToken() != null;
  39. final List<Widget> contents = [];
  40. if (hasLoggedIn) {
  41. contents.addAll([
  42. BackupSettingsWidget(),
  43. Padding(padding: EdgeInsets.all(12)),
  44. ]);
  45. }
  46. contents.addAll([
  47. SecuritySectionWidget(),
  48. Padding(padding: EdgeInsets.all(12)),
  49. SupportSectionWidget(),
  50. Padding(padding: EdgeInsets.all(12)),
  51. InfoSectionWidget(),
  52. ]);
  53. if (hasLoggedIn) {
  54. contents.add(AccountSectionWidget());
  55. }
  56. contents.add(
  57. FutureBuilder(
  58. future: _getAppVersion(),
  59. builder: (context, snapshot) {
  60. if (snapshot.hasData) {
  61. return Padding(
  62. padding: const EdgeInsets.all(16.0),
  63. child: Text(
  64. "app version: " + snapshot.data,
  65. style: TextStyle(
  66. fontSize: 12,
  67. color: Colors.white.withOpacity(0.6),
  68. ),
  69. ),
  70. );
  71. }
  72. return Container();
  73. },
  74. ),
  75. );
  76. if (kDebugMode && hasLoggedIn) {
  77. contents.add(DebugWidget());
  78. }
  79. return SingleChildScrollView(
  80. child: Padding(
  81. padding: const EdgeInsets.all(12.0),
  82. child: Column(
  83. children: contents,
  84. ),
  85. ),
  86. );
  87. }
  88. static Future<String> _getAppVersion() async {
  89. var pkgInfo = await PackageInfo.fromPlatform();
  90. return "${pkgInfo.version}";
  91. }
  92. }
  93. class BackupSettingsWidget extends StatefulWidget {
  94. BackupSettingsWidget({Key key}) : super(key: key);
  95. @override
  96. BackupSettingsWidgetState createState() => BackupSettingsWidgetState();
  97. }
  98. class BackupSettingsWidgetState extends State<BackupSettingsWidget> {
  99. double _usageInGBs;
  100. @override
  101. void initState() {
  102. _getUsage();
  103. super.initState();
  104. }
  105. @override
  106. Widget build(BuildContext context) {
  107. return Container(
  108. child: Column(
  109. children: [
  110. SettingsSectionTitle("backup"),
  111. GestureDetector(
  112. behavior: HitTestBehavior.translucent,
  113. onTap: () async {
  114. _showFoldersDialog(context);
  115. },
  116. child: SettingsTextItem(
  117. text: "backed up folders", icon: Icons.navigate_next),
  118. ),
  119. Divider(height: 4),
  120. Padding(padding: EdgeInsets.all(4)),
  121. Container(
  122. height: 36,
  123. child: Row(
  124. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  125. children: [
  126. Text("backup over mobile data"),
  127. Switch(
  128. value: Configuration.instance.shouldBackupOverMobileData(),
  129. onChanged: (value) async {
  130. Configuration.instance.setBackupOverMobileData(value);
  131. setState(() {});
  132. },
  133. ),
  134. ],
  135. ),
  136. ),
  137. Padding(padding: EdgeInsets.all(4)),
  138. Divider(height: 4),
  139. GestureDetector(
  140. behavior: HitTestBehavior.translucent,
  141. onTap: () async {
  142. Navigator.of(context).push(
  143. MaterialPageRoute(
  144. builder: (BuildContext context) {
  145. return SubscriptionPage();
  146. },
  147. ),
  148. );
  149. },
  150. child: SettingsTextItem(
  151. text: "subscription plan", icon: Icons.navigate_next),
  152. ),
  153. Divider(height: 4),
  154. Padding(padding: EdgeInsets.all(8)),
  155. Row(
  156. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  157. children: [
  158. Text("total data backed up"),
  159. Container(
  160. height: 20,
  161. child: _usageInGBs == null
  162. ? loadWidget
  163. : Text(
  164. _usageInGBs.toString() + " GB",
  165. ),
  166. ),
  167. ],
  168. ),
  169. ],
  170. ),
  171. );
  172. }
  173. void _showFoldersDialog(BuildContext context) async {
  174. AlertDialog alert = AlertDialog(
  175. title: Text("select folders to back up"),
  176. content: BackedUpFoldersWidget(),
  177. actions: [
  178. FlatButton(
  179. child: Text("OK"),
  180. onPressed: () {
  181. Navigator.of(context, rootNavigator: true).pop('dialog');
  182. },
  183. ),
  184. ],
  185. );
  186. showDialog(
  187. context: context,
  188. builder: (BuildContext context) {
  189. return alert;
  190. },
  191. );
  192. }
  193. void _getUsage() {
  194. BillingService.instance.fetchUsage().then((usage) async {
  195. if (mounted) {
  196. setState(() {
  197. _usageInGBs = convertBytesToGBs(usage);
  198. });
  199. }
  200. });
  201. }
  202. }
  203. class BackedUpFoldersWidget extends StatefulWidget {
  204. const BackedUpFoldersWidget({
  205. Key key,
  206. }) : super(key: key);
  207. @override
  208. _BackedUpFoldersWidgetState createState() => _BackedUpFoldersWidgetState();
  209. }
  210. class _BackedUpFoldersWidgetState extends State<BackedUpFoldersWidget> {
  211. @override
  212. Widget build(BuildContext context) {
  213. return FutureBuilder<List<String>>(
  214. future: FilesDB.instance.getLocalPaths(),
  215. builder: (context, snapshot) {
  216. if (snapshot.hasData) {
  217. snapshot.data.sort((first, second) {
  218. return first.toLowerCase().compareTo(second.toLowerCase());
  219. });
  220. final backedUpFolders = Configuration.instance.getPathsToBackUp();
  221. final foldersWidget = List<Widget>();
  222. for (final folder in snapshot.data) {
  223. foldersWidget.add(CheckboxListTile(
  224. value: backedUpFolders.contains(folder),
  225. dense: true,
  226. title: Text(folder),
  227. controlAffinity: ListTileControlAffinity.leading,
  228. onChanged: (value) async {
  229. if (value) {
  230. backedUpFolders.add(folder);
  231. } else {
  232. backedUpFolders.remove(folder);
  233. }
  234. await Configuration.instance.setPathsToBackUp(backedUpFolders);
  235. setState(() {});
  236. },
  237. ));
  238. }
  239. final scrollController = ScrollController();
  240. return Container(
  241. child: Scrollbar(
  242. isAlwaysShown: true,
  243. controller: scrollController,
  244. child: SingleChildScrollView(
  245. child: Column(children: foldersWidget),
  246. controller: scrollController,
  247. ),
  248. ),
  249. );
  250. }
  251. return loadWidget;
  252. },
  253. );
  254. }
  255. }
  256. class SecuritySectionWidget extends StatefulWidget {
  257. SecuritySectionWidget({Key key}) : super(key: key);
  258. @override
  259. _SecuritySectionWidgetState createState() => _SecuritySectionWidgetState();
  260. }
  261. class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
  262. @override
  263. void initState() {
  264. super.initState();
  265. }
  266. @override
  267. Widget build(BuildContext context) {
  268. final List<Widget> children = [];
  269. children.addAll([
  270. SettingsSectionTitle("security"),
  271. Container(
  272. height: 36,
  273. child: Row(
  274. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  275. children: [
  276. Text("lockscreen"),
  277. Switch(
  278. value: Configuration.instance.shouldShowLockScreen(),
  279. onChanged: (value) async {
  280. AppLock.of(context).disable();
  281. final result = await requestAuthentication();
  282. if (result) {
  283. AppLock.of(context).setEnabled(value);
  284. Configuration.instance.setShouldShowLockScreen(value);
  285. setState(() {});
  286. } else {
  287. AppLock.of(context).setEnabled(
  288. Configuration.instance.shouldShowLockScreen());
  289. }
  290. },
  291. ),
  292. ],
  293. ),
  294. ),
  295. ]);
  296. if (Platform.isAndroid) {
  297. children.addAll([
  298. Padding(padding: EdgeInsets.all(4)),
  299. Divider(height: 4),
  300. Padding(padding: EdgeInsets.all(4)),
  301. Container(
  302. height: 36,
  303. child: Row(
  304. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  305. children: [
  306. Text("hide from recents"),
  307. Switch(
  308. value: Configuration.instance.shouldHideFromRecents(),
  309. onChanged: (value) async {
  310. if (value) {
  311. AlertDialog alert = AlertDialog(
  312. title: Text("hide from recents?"),
  313. content: SingleChildScrollView(
  314. child: Column(
  315. mainAxisAlignment: MainAxisAlignment.start,
  316. crossAxisAlignment: CrossAxisAlignment.start,
  317. children: [
  318. Text(
  319. "hiding from the task switcher will prevent you from taking screenshots in this app.",
  320. style: TextStyle(
  321. height: 1.5,
  322. ),
  323. ),
  324. Padding(padding: EdgeInsets.all(8)),
  325. Text(
  326. "are you sure?",
  327. style: TextStyle(
  328. height: 1.5,
  329. ),
  330. ),
  331. ],
  332. ),
  333. ),
  334. actions: [
  335. TextButton(
  336. child:
  337. Text("no", style: TextStyle(color: Colors.white)),
  338. onPressed: () {
  339. Navigator.of(context, rootNavigator: true)
  340. .pop('dialog');
  341. },
  342. ),
  343. TextButton(
  344. child: Text("yes",
  345. style: TextStyle(
  346. color: Colors.white.withOpacity(0.8))),
  347. onPressed: () async {
  348. Navigator.of(context, rootNavigator: true)
  349. .pop('dialog');
  350. await Configuration.instance
  351. .setShouldHideFromRecents(true);
  352. await FlutterWindowManager.addFlags(
  353. FlutterWindowManager.FLAG_SECURE);
  354. setState(() {});
  355. },
  356. ),
  357. ],
  358. );
  359. showDialog(
  360. context: context,
  361. builder: (BuildContext context) {
  362. return alert;
  363. },
  364. );
  365. } else {
  366. await Configuration.instance
  367. .setShouldHideFromRecents(false);
  368. await FlutterWindowManager.clearFlags(
  369. FlutterWindowManager.FLAG_SECURE);
  370. setState(() {});
  371. }
  372. },
  373. ),
  374. ],
  375. ),
  376. ),
  377. ]);
  378. }
  379. return Container(
  380. child: Column(
  381. children: children,
  382. ),
  383. );
  384. }
  385. }
  386. class SettingsSectionTitle extends StatelessWidget {
  387. final String title;
  388. const SettingsSectionTitle(this.title, {Key key}) : super(key: key);
  389. @override
  390. Widget build(BuildContext context) {
  391. return Container(
  392. child: Column(children: [
  393. Padding(padding: EdgeInsets.all(4)),
  394. Align(
  395. alignment: Alignment.centerLeft,
  396. child: Text(
  397. title,
  398. style: TextStyle(
  399. fontWeight: FontWeight.bold,
  400. fontSize: 16,
  401. color: Theme.of(context).accentColor,
  402. ),
  403. ),
  404. ),
  405. Padding(padding: EdgeInsets.all(4)),
  406. ]));
  407. }
  408. }
  409. class SupportSectionWidget extends StatelessWidget {
  410. const SupportSectionWidget({Key key}) : super(key: key);
  411. @override
  412. Widget build(BuildContext context) {
  413. return Container(
  414. child: Column(children: [
  415. SettingsSectionTitle("support"),
  416. GestureDetector(
  417. behavior: HitTestBehavior.translucent,
  418. onTap: () {
  419. Navigator.of(context).push(
  420. MaterialPageRoute(
  421. builder: (BuildContext context) {
  422. final endpoint = Configuration.instance.getHttpEndpoint() +
  423. "/users/roadmap";
  424. final isLoggedIn = Configuration.instance.getToken() != null;
  425. final url = isLoggedIn
  426. ? endpoint + "?token=" + Configuration.instance.getToken()
  427. : ROADMAP_URL;
  428. return WebPage("roadmap", url);
  429. },
  430. ),
  431. );
  432. },
  433. child: SettingsTextItem(text: "roadmap", icon: Icons.navigate_next),
  434. ),
  435. Divider(height: 4),
  436. GestureDetector(
  437. behavior: HitTestBehavior.translucent,
  438. onTap: () async {
  439. final Email email = Email(
  440. recipients: ['hey@ente.io'],
  441. isHTML: false,
  442. );
  443. try {
  444. await FlutterEmailSender.send(email);
  445. } catch (e) {
  446. showGenericErrorDialog(context);
  447. }
  448. },
  449. onLongPress: () async {
  450. showToast("thanks for reporting a bug!");
  451. final dialog = createProgressDialog(context, "preparing logs...");
  452. await dialog.show();
  453. final tempPath = (await getTemporaryDirectory()).path;
  454. final zipFilePath = tempPath + "/logs.zip";
  455. final logsDirectory = Directory(tempPath + "/logs");
  456. var encoder = ZipFileEncoder();
  457. encoder.create(zipFilePath);
  458. encoder.addDirectory(logsDirectory);
  459. encoder.close();
  460. await dialog.hide();
  461. final Email email = Email(
  462. recipients: ['bug@ente.io'],
  463. attachmentPaths: [zipFilePath],
  464. isHTML: false,
  465. );
  466. try {
  467. await FlutterEmailSender.send(email);
  468. } catch (e) {
  469. return Share.shareFiles([zipFilePath]);
  470. }
  471. },
  472. child: SettingsTextItem(text: "email", icon: Icons.navigate_next),
  473. ),
  474. Divider(height: 4),
  475. GestureDetector(
  476. behavior: HitTestBehavior.translucent,
  477. onTap: () async {
  478. Navigator.of(context).push(
  479. MaterialPageRoute(
  480. builder: (BuildContext context) {
  481. return CrispChatPage();
  482. },
  483. ),
  484. );
  485. },
  486. child: SettingsTextItem(text: "chat", icon: Icons.navigate_next),
  487. ),
  488. ]),
  489. );
  490. }
  491. }
  492. class InfoSectionWidget extends StatelessWidget {
  493. const InfoSectionWidget({Key key}) : super(key: key);
  494. @override
  495. Widget build(BuildContext context) {
  496. return Container(
  497. child: Column(children: [
  498. SettingsSectionTitle("about"),
  499. GestureDetector(
  500. behavior: HitTestBehavior.translucent,
  501. onTap: () async {
  502. Navigator.of(context).push(
  503. MaterialPageRoute(
  504. builder: (BuildContext context) {
  505. return WebPage("faq", "https://ente.io/faq");
  506. },
  507. ),
  508. );
  509. },
  510. child: SettingsTextItem(text: "faq", icon: Icons.navigate_next),
  511. ),
  512. Divider(height: 4),
  513. GestureDetector(
  514. behavior: HitTestBehavior.translucent,
  515. onTap: () {
  516. Navigator.of(context).push(
  517. MaterialPageRoute(
  518. builder: (BuildContext context) {
  519. return WebPage("terms", "https://ente.io/terms");
  520. },
  521. ),
  522. );
  523. },
  524. child: SettingsTextItem(text: "terms", icon: Icons.navigate_next),
  525. ),
  526. Divider(height: 4),
  527. GestureDetector(
  528. behavior: HitTestBehavior.translucent,
  529. onTap: () {
  530. Navigator.of(context).push(
  531. MaterialPageRoute(
  532. builder: (BuildContext context) {
  533. return WebPage("privacy", "https://ente.io/privacy");
  534. },
  535. ),
  536. );
  537. },
  538. child: SettingsTextItem(text: "privacy", icon: Icons.navigate_next),
  539. ),
  540. Divider(height: 4),
  541. GestureDetector(
  542. behavior: HitTestBehavior.translucent,
  543. onTap: () async {
  544. launch("https://github.com/ente-io/frame");
  545. },
  546. child:
  547. SettingsTextItem(text: "source code", icon: Icons.navigate_next),
  548. ),
  549. ]),
  550. );
  551. }
  552. }
  553. class AccountSectionWidget extends StatelessWidget {
  554. const AccountSectionWidget({Key key}) : super(key: key);
  555. @override
  556. Widget build(BuildContext context) {
  557. return Container(
  558. child: Column(children: [
  559. Padding(padding: EdgeInsets.all(12)),
  560. SettingsSectionTitle("account"),
  561. GestureDetector(
  562. behavior: HitTestBehavior.translucent,
  563. onTap: () async {
  564. AlertDialog alert = AlertDialog(
  565. title: Text("logout"),
  566. content: Text("are you sure you want to logout?"),
  567. actions: [
  568. TextButton(
  569. child: Text(
  570. "no",
  571. style: TextStyle(
  572. color: Theme.of(context).buttonColor,
  573. ),
  574. ),
  575. onPressed: () {
  576. Navigator.of(context, rootNavigator: true).pop('dialog');
  577. },
  578. ),
  579. TextButton(
  580. child: Text(
  581. "yes, logout",
  582. style: TextStyle(
  583. color: Colors.red,
  584. ),
  585. ),
  586. onPressed: () async {
  587. Navigator.of(context, rootNavigator: true).pop('dialog');
  588. final dialog =
  589. createProgressDialog(context, "logging out...");
  590. await dialog.show();
  591. await Configuration.instance.logout();
  592. await dialog.hide();
  593. Navigator.of(context).popUntil((route) => route.isFirst);
  594. },
  595. ),
  596. ],
  597. );
  598. showDialog(
  599. context: context,
  600. builder: (BuildContext context) {
  601. return alert;
  602. },
  603. );
  604. },
  605. child: SettingsTextItem(text: "logout", icon: Icons.navigate_next),
  606. ),
  607. ]),
  608. );
  609. }
  610. }
  611. class DebugWidget extends StatelessWidget {
  612. const DebugWidget({Key key}) : super(key: key);
  613. @override
  614. Widget build(BuildContext context) {
  615. return Container(
  616. child: Column(children: [
  617. Padding(padding: EdgeInsets.all(12)),
  618. SettingsSectionTitle("debug"),
  619. GestureDetector(
  620. behavior: HitTestBehavior.translucent,
  621. onTap: () async {
  622. _showKeyAttributesDialog(context);
  623. },
  624. child: SettingsTextItem(
  625. text: "key attributes", icon: Icons.navigate_next),
  626. ),
  627. ]),
  628. );
  629. }
  630. void _showKeyAttributesDialog(BuildContext context) {
  631. final keyAttributes = Configuration.instance.getKeyAttributes();
  632. AlertDialog alert = AlertDialog(
  633. title: Text("key attributes"),
  634. content: SingleChildScrollView(
  635. child: Column(children: [
  636. Text("Key", style: TextStyle(fontWeight: FontWeight.bold)),
  637. Text(Sodium.bin2base64(Configuration.instance.getKey())),
  638. Padding(padding: EdgeInsets.all(12)),
  639. Text("Encrypted Key", style: TextStyle(fontWeight: FontWeight.bold)),
  640. Text(keyAttributes.encryptedKey),
  641. Padding(padding: EdgeInsets.all(12)),
  642. Text("Key Decryption Nonce",
  643. style: TextStyle(fontWeight: FontWeight.bold)),
  644. Text(keyAttributes.keyDecryptionNonce),
  645. Padding(padding: EdgeInsets.all(12)),
  646. Text("KEK Salt", style: TextStyle(fontWeight: FontWeight.bold)),
  647. Text(keyAttributes.kekSalt),
  648. Padding(padding: EdgeInsets.all(12)),
  649. ]),
  650. ),
  651. actions: [
  652. FlatButton(
  653. child: Text("OK"),
  654. onPressed: () {
  655. Navigator.of(context, rootNavigator: true).pop('dialog');
  656. },
  657. ),
  658. ],
  659. );
  660. showDialog(
  661. context: context,
  662. builder: (BuildContext context) {
  663. return alert;
  664. },
  665. );
  666. }
  667. }
  668. class SettingsTextItem extends StatelessWidget {
  669. final String text;
  670. final IconData icon;
  671. const SettingsTextItem({
  672. Key key,
  673. @required this.text,
  674. @required this.icon,
  675. }) : super(key: key);
  676. @override
  677. Widget build(BuildContext context) {
  678. return Column(
  679. children: [
  680. Padding(padding: EdgeInsets.all(Platform.isIOS ? 4 : 6)),
  681. Row(
  682. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  683. children: [
  684. Align(alignment: Alignment.centerLeft, child: Text(text)),
  685. Icon(icon),
  686. ],
  687. ),
  688. Padding(padding: EdgeInsets.all(Platform.isIOS ? 2 : 6)),
  689. ],
  690. );
  691. }
  692. }
  693. class CrispChatPage extends StatefulWidget {
  694. CrispChatPage({Key key}) : super(key: key);
  695. @override
  696. _CrispChatPageState createState() => _CrispChatPageState();
  697. }
  698. class _CrispChatPageState extends State<CrispChatPage> {
  699. static const websiteID = "86d56ea2-68a2-43f9-8acb-95e06dee42e8";
  700. @override
  701. void initState() {
  702. crisp.initialize(
  703. websiteId: websiteID,
  704. );
  705. crisp.register(
  706. CrispUser(
  707. email: Configuration.instance.getEmail(),
  708. ),
  709. );
  710. super.initState();
  711. }
  712. @override
  713. Widget build(BuildContext context) {
  714. return Scaffold(
  715. appBar: AppBar(
  716. title: Text("support chat"),
  717. ),
  718. body: CrispView(
  719. loadingWidget: loadWidget,
  720. ),
  721. );
  722. }
  723. }