Web implementation of shared_preferences (#2332)

Implement `shared_preferences` for the Web.
diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md
new file mode 100644
index 0000000..08378c6
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md
@@ -0,0 +1,3 @@
+# 0.1.0
+
+- Initial release.
diff --git a/packages/shared_preferences/shared_preferences_web/LICENSE b/packages/shared_preferences/shared_preferences_web/LICENSE
new file mode 100644
index 0000000..0c382ce
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/LICENSE
@@ -0,0 +1,27 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//    * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//    * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//    * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/shared_preferences/shared_preferences_web/README.md b/packages/shared_preferences/shared_preferences_web/README.md
new file mode 100644
index 0000000..8f9d22d
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/README.md
@@ -0,0 +1,32 @@
+# shared_preferences_web
+
+The web implementation of [`shared_preferences`][1].
+
+## Usage
+
+### Import the package
+
+To use this plugin in your Flutter Web app, simply add it as a dependency in
+your `pubspec.yaml` alongside the base `shared_preferences` plugin.
+
+_(This is only temporary: in the future we hope to make this package an
+"endorsed" implementation of `shared_preferences`, so that it is automatically
+included in your Flutter Web app when you depend on `package:shared_preferences`.)_
+
+This is what the above means to your `pubspec.yaml`:
+
+```yaml
+...
+dependencies:
+  ...
+  shared_preferences: ^0.5.4+8
+  shared_preferences_web: ^0.1.0
+  ...
+```
+
+### Use the plugin
+
+Once you have the `shared_preferences_web` dependency in your pubspec, you should
+be able to use `package:shared_preferences` as normal.
+
+[1]: ../shared_preferences/shared_preferences
diff --git a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart
new file mode 100644
index 0000000..8a0f137
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart
@@ -0,0 +1,91 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert' show json;
+import 'dart:html' as html;
+
+import 'package:flutter_web_plugins/flutter_web_plugins.dart';
+import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
+
+/// The web implementation of [SharedPreferencesStorePlatform].
+///
+/// This class implements the `package:shared_preferences` functionality for the web.
+class SharedPreferencesPlugin extends SharedPreferencesStorePlatform {
+  /// Registers this class as the default instance of [SharedPreferencesStorePlatform].
+  static void registerWith(Registrar registrar) {
+    SharedPreferencesStorePlatform.instance = SharedPreferencesPlugin();
+  }
+
+  @override
+  Future<bool> clear() async {
+    // IMPORTANT: Do not use html.window.localStorage.clear() as that will
+    //            remove _all_ local data, not just the keys prefixed with
+    //            "flutter."
+    for (String key in _storedFlutterKeys) {
+      html.window.localStorage.remove(key);
+    }
+    return true;
+  }
+
+  @override
+  Future<Map<String, Object>> getAll() async {
+    final Map<String, Object> allData = <String, Object>{};
+    for (String key in _storedFlutterKeys) {
+      allData[key] = _decodeValue(html.window.localStorage[key]);
+    }
+    return allData;
+  }
+
+  @override
+  Future<bool> remove(String key) async {
+    _checkPrefix(key);
+    html.window.localStorage.remove(key);
+    return true;
+  }
+
+  @override
+  Future<bool> setValue(String valueType, String key, Object value) async {
+    _checkPrefix(key);
+    html.window.localStorage[key] = _encodeValue(value);
+    return true;
+  }
+
+  void _checkPrefix(String key) {
+    if (!key.startsWith('flutter.')) {
+      throw FormatException(
+        'Shared preferences keys must start with prefix "flutter.".',
+        key,
+        0,
+      );
+    }
+  }
+
+  List<String> get _storedFlutterKeys {
+    final List<String> keys = <String>[];
+    for (String key in html.window.localStorage.keys) {
+      if (key.startsWith('flutter.')) {
+        keys.add(key);
+      }
+    }
+    return keys;
+  }
+
+  String _encodeValue(Object value) {
+    return json.encode(value);
+  }
+
+  Object _decodeValue(String encodedValue) {
+    final Object decodedValue = json.decode(encodedValue);
+
+    if (decodedValue is List) {
+      // JSON does not preserve generics. The encode/decode roundtrip is
+      // `List<String>` => JSON => `List<dynamic>`. We have to explicitly
+      // restore the RTTI.
+      return decodedValue.cast<String>();
+    }
+
+    return decodedValue;
+  }
+}
diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml
new file mode 100644
index 0000000..b2ba90a
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml
@@ -0,0 +1,28 @@
+name: shared_preferences_web
+description: Web platform implementation of shared_preferences
+author: Flutter Team <flutter-dev@googlegroups.com>
+homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_web
+version: 0.1.0
+
+flutter:
+  plugin:
+    platforms:
+      web:
+        pluginClass: SharedPreferencesPlugin
+        fileName: shared_preferences_web.dart
+
+dependencies:
+  shared_preferences_platform_interface: ^1.0.0
+  flutter:
+    sdk: flutter
+  flutter_web_plugins:
+    sdk: flutter
+  meta: ^1.1.7
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+environment:
+  sdk: ">=2.0.0-dev.28.0 <3.0.0"
+  flutter: ">=1.5.0 <2.0.0"
diff --git a/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart b/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart
new file mode 100644
index 0000000..951f04c
--- /dev/null
+++ b/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart
@@ -0,0 +1,89 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@TestOn('chrome') // Uses web-only Flutter SDK
+
+import 'dart:convert' show json;
+import 'dart:html' as html;
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart';
+import 'package:shared_preferences_web/shared_preferences_web.dart';
+
+const Map<String, dynamic> kTestValues = <String, dynamic>{
+  'flutter.String': 'hello world',
+  'flutter.Bool': true,
+  'flutter.Int': 42,
+  'flutter.Double': 3.14159,
+  'flutter.StringList': <String>['foo', 'bar'],
+};
+
+void main() {
+  group('SharedPreferencesPlugin', () {
+    setUp(() {
+      html.window.localStorage.clear();
+    });
+
+    test('registers itself', () {
+      expect(SharedPreferencesStorePlatform.instance,
+          isNot(isA<SharedPreferencesPlugin>()));
+      SharedPreferencesPlugin.registerWith(null);
+      expect(SharedPreferencesStorePlatform.instance,
+          isA<SharedPreferencesPlugin>());
+    });
+
+    test('getAll', () async {
+      final SharedPreferencesPlugin store = SharedPreferencesPlugin();
+      expect(await store.getAll(), isEmpty);
+
+      html.window.localStorage['flutter.testKey'] = '"test value"';
+      html.window.localStorage['unprefixed_key'] = 'not a flutter value';
+      final Map<String, Object> allData = await store.getAll();
+      expect(allData, hasLength(1));
+      expect(allData['flutter.testKey'], 'test value');
+    });
+
+    test('remove', () async {
+      final SharedPreferencesPlugin store = SharedPreferencesPlugin();
+      html.window.localStorage['flutter.testKey'] = '"test value"';
+      expect(html.window.localStorage['flutter.testKey'], isNotNull);
+      expect(await store.remove('flutter.testKey'), isTrue);
+      expect(html.window.localStorage['flutter.testKey'], isNull);
+      expect(
+        () => store.remove('unprefixed'),
+        throwsA(isA<FormatException>()),
+      );
+    });
+
+    test('setValue', () async {
+      final SharedPreferencesPlugin store = SharedPreferencesPlugin();
+      for (String key in kTestValues.keys) {
+        final dynamic value = kTestValues[key];
+        expect(await store.setValue(key.split('.').last, key, value), true);
+      }
+      expect(html.window.localStorage.keys, hasLength(kTestValues.length));
+      for (String key in html.window.localStorage.keys) {
+        expect(html.window.localStorage[key], json.encode(kTestValues[key]));
+      }
+
+      // Check that generics are preserved.
+      expect((await store.getAll())['flutter.StringList'], isA<List<String>>());
+
+      // Invalid key format.
+      expect(
+        () => store.setValue('String', 'unprefixed', 'hello'),
+        throwsA(isA<FormatException>()),
+      );
+    });
+
+    test('clear', () async {
+      final SharedPreferencesPlugin store = SharedPreferencesPlugin();
+      html.window.localStorage['flutter.testKey1'] = '"test value"';
+      html.window.localStorage['flutter.testKey2'] = '42';
+      html.window.localStorage['unprefixed_key'] = 'not a flutter value';
+      expect(await store.clear(), isTrue);
+      expect(html.window.localStorage.keys.single, 'unprefixed_key');
+    });
+  });
+}