webViewRunner.dart 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:flutter/services.dart';
  4. import 'package:flutter_inappwebview/flutter_inappwebview.dart';
  5. import 'package:polkawallet_sdk/api/types/networkParams.dart';
  6. import 'package:polkawallet_sdk/service/keyring.dart';
  7. import 'package:polkawallet_sdk/storage/keyring.dart';
  8. class WebViewRunner {
  9. HeadlessInAppWebView? _web;
  10. Function? _onLaunched;
  11. late String _jsCode;
  12. late String _jsCodeEth;
  13. Map<String, Function> _msgHandlers = {};
  14. Map<String, Completer> _msgCompleters = {};
  15. int _evalJavascriptUID = 0;
  16. bool webViewLoaded = false;
  17. int jsCodeStarted = -1;
  18. Timer? _webViewReloadTimer;
  19. Future<void> launch(
  20. ServiceKeyring? keyring,
  21. Keyring keyringStorage,
  22. Function? onLaunched, {
  23. String? jsCode,
  24. Function? socketDisconnectedAction,
  25. }) async {
  26. /// reset state before webView launch or reload
  27. _msgHandlers = {};
  28. _msgCompleters = {};
  29. _evalJavascriptUID = 0;
  30. _onLaunched = onLaunched;
  31. webViewLoaded = false;
  32. jsCodeStarted = -1;
  33. _jsCode = jsCode ??
  34. await rootBundle
  35. .loadString('packages/polkawallet_sdk/js_api/dist/main.js');
  36. print('js file loaded');
  37. _jsCodeEth = await rootBundle
  38. .loadString('packages/polkawallet_sdk/js_api_eth/dist/main.js');
  39. print('js eth file loaded');
  40. if (_web == null) {
  41. await _startLocalServer();
  42. _web = new HeadlessInAppWebView(
  43. initialOptions: InAppWebViewGroupOptions(
  44. crossPlatform: InAppWebViewOptions(clearCache: true),
  45. ),
  46. initialUrlRequest: URLRequest(
  47. url: Uri.parse("http://localhost:8080/assets/index.html")),
  48. onWebViewCreated: (controller) {
  49. print('HeadlessInAppWebView created!');
  50. },
  51. onConsoleMessage: (controller, message) {
  52. print("CONSOLE MESSAGE: " + message.message);
  53. if (jsCodeStarted < 0) {
  54. if (message.message.contains('js loaded')) {
  55. jsCodeStarted = 1;
  56. } else {
  57. jsCodeStarted = 0;
  58. }
  59. }
  60. if (message.message.contains("WebSocket is not connected") &&
  61. socketDisconnectedAction != null) {
  62. socketDisconnectedAction();
  63. }
  64. if (message.messageLevel != ConsoleMessageLevel.LOG) return;
  65. try {
  66. var msg = jsonDecode(message.message);
  67. final String? path = msg['path'];
  68. if (_msgCompleters[path!] != null) {
  69. Completer handler = _msgCompleters[path]!;
  70. handler.complete(msg['data']);
  71. if (path.contains('uid=')) {
  72. _msgCompleters.remove(path);
  73. }
  74. }
  75. if (_msgHandlers[path] != null) {
  76. Function handler = _msgHandlers[path]!;
  77. handler(msg['data']);
  78. }
  79. } catch (_) {
  80. // ignore
  81. }
  82. },
  83. onLoadStop: (controller, url) async {
  84. print('webview loaded $url');
  85. if (webViewLoaded) return;
  86. _handleReloaded();
  87. await _startJSCode(keyring, keyringStorage);
  88. },
  89. );
  90. await _web?.dispose();
  91. await _web?.run();
  92. } else {
  93. _webViewReloadTimer = Timer.periodic(Duration(seconds: 3), (timer) {
  94. _tryReload();
  95. });
  96. }
  97. }
  98. void _tryReload() {
  99. if (!webViewLoaded) {
  100. _web?.webViewController.reload();
  101. }
  102. }
  103. void _handleReloaded() {
  104. _webViewReloadTimer?.cancel();
  105. webViewLoaded = true;
  106. }
  107. Future<void> _startLocalServer() async {
  108. final localhostServer = new InAppLocalhostServer();
  109. await localhostServer.start();
  110. }
  111. Future<void> _startJSCode(
  112. ServiceKeyring? keyring, Keyring keyringStorage) async {
  113. // inject js file to webView
  114. await _web!.webViewController.evaluateJavascript(source: _jsCode);
  115. await _web!.webViewController.evaluateJavascript(source: _jsCodeEth);
  116. _onLaunched!();
  117. }
  118. int getEvalJavascriptUID() {
  119. return _evalJavascriptUID++;
  120. }
  121. Future<dynamic> evalJavascript(
  122. String code, {
  123. bool wrapPromise = true,
  124. bool allowRepeat = true,
  125. }) async {
  126. // check if there's a same request loading
  127. if (!allowRepeat) {
  128. for (String i in _msgCompleters.keys) {
  129. String call = code.split('(')[0];
  130. if (i.contains(call)) {
  131. print('request $call loading');
  132. return _msgCompleters[i]!.future;
  133. }
  134. }
  135. }
  136. if (!wrapPromise) {
  137. final res =
  138. await _web!.webViewController.evaluateJavascript(source: code);
  139. return res;
  140. }
  141. final c = new Completer();
  142. final uid = getEvalJavascriptUID();
  143. final method = 'uid=$uid;${code.split('(')[0]}';
  144. _msgCompleters[method] = c;
  145. final script = '$code.then(function(res) {'
  146. ' console.log(JSON.stringify({ path: "$method", data: res }));'
  147. '}).catch(function(err) {'
  148. ' console.log(JSON.stringify({ path: "log", data: {call: "$method", error: err.message} }));'
  149. '});';
  150. _web!.webViewController.evaluateJavascript(source: script);
  151. return c.future;
  152. }
  153. Future<NetworkParams?> connectNode(List<NetworkParams> nodes) async {
  154. final isAvatarSupport = (await evalJavascript(
  155. 'settings.connectAll ? {}:null',
  156. wrapPromise: false)) !=
  157. null;
  158. final dynamic res = await (isAvatarSupport
  159. ? evalJavascript(
  160. 'settings.connectAll(${jsonEncode(nodes.map((e) => e.endpoint).toList())})')
  161. : evalJavascript(
  162. 'settings.connect(${jsonEncode(nodes.map((e) => e.endpoint).toList())})'));
  163. if (res != null) {
  164. final index = nodes.indexWhere((e) => e.endpoint!.trim() == res.trim());
  165. return nodes[index > -1 ? index : 0];
  166. }
  167. return null;
  168. }
  169. Future<void> subscribeMessage(
  170. String code,
  171. String channel,
  172. Function callback,
  173. ) async {
  174. addMsgHandler(channel, callback);
  175. evalJavascript(code);
  176. }
  177. void unsubscribeMessage(String channel) {
  178. print('unsubscribe $channel');
  179. final unsubCall = 'unsub$channel';
  180. _web!.webViewController
  181. .evaluateJavascript(source: 'window.$unsubCall && window.$unsubCall()');
  182. }
  183. void addMsgHandler(String channel, Function onMessage) {
  184. _msgHandlers[channel] = onMessage;
  185. }
  186. void removeMsgHandler(String channel) {
  187. _msgHandlers.remove(channel);
  188. }
  189. }