MOBILE-3839 browser: Support SecureStorage in browser
parent
c4ce1edfe0
commit
7e7aae4853
|
@ -12,12 +12,12 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { SecureStorage } from '../src/ts/plugins/SecureStorage';
|
||||
import { SecureStorage as SecureStorageImpl } from '../src/ts/plugins/SecureStorage';
|
||||
|
||||
declare global {
|
||||
|
||||
interface MoodleAppPlugins {
|
||||
secureStorage: SecureStorage;
|
||||
secureStorage: SecureStorageImpl;
|
||||
}
|
||||
|
||||
interface Cordova {
|
||||
|
@ -25,3 +25,5 @@ declare global {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
export type SecureStorage = InstanceType<typeof SecureStorageImpl>;
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { SecureStorage } from 'cordova-plugin-moodleapp';
|
||||
|
||||
/**
|
||||
* Mock for SecureStorage plugin. It will store the data without being encrypted.
|
||||
*/
|
||||
export class SecureStorageMock implements SecureStorage {
|
||||
|
||||
/**
|
||||
* Get one or more values.
|
||||
*
|
||||
* @param names Names of the values to get.
|
||||
* @param collection The collection where the values are stored.
|
||||
* @returns Object with name -> value. If a name isn't found it won't be included in the result.
|
||||
*/
|
||||
async get(names: string | string[], collection: string): Promise<Record<string, string>> {
|
||||
if (typeof names === 'string') {
|
||||
names = [names];
|
||||
}
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const name = names[i];
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const storedValue = localStorage.getItem(this.getPrefixedName(name, collection));
|
||||
if (storedValue !== null) {
|
||||
result[name] = storedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the prefix to add to a name, including the collection.
|
||||
*
|
||||
* @param collection Collection name.
|
||||
* @returns Prefix.
|
||||
*/
|
||||
private getCollectionPrefix(collection: string): string {
|
||||
return `SecureStorage_${collection}_`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full name to retrieve, store or delete an item.
|
||||
*
|
||||
* @param name Name inside collection.
|
||||
* @param collection Collection name.
|
||||
* @returns Full name.
|
||||
*/
|
||||
private getPrefixedName(name: string, collection: string): string {
|
||||
return this.getCollectionPrefix(collection) + name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set one or more values.
|
||||
*
|
||||
* @param data Object with values to store, in format name -> value. Null or undefined valid values will be ignored.
|
||||
* @param collection The collection where to store the values.
|
||||
*/
|
||||
async store(data: Record<string, string>, collection: string): Promise<void> {
|
||||
for (const name in data) {
|
||||
const value = data[name];
|
||||
if (value === undefined || value === null) {
|
||||
delete data[name];
|
||||
} else if (typeof value !== 'string') {
|
||||
throw new Error(`SecureStorage: Invalid value for ${name}. Expected string, received ${typeof value}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const name in data) {
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = data[name];
|
||||
localStorage.setItem(this.getPrefixedName(name, collection), value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete one or more values.
|
||||
*
|
||||
* @param names Names to delete.
|
||||
* @param collection The collection where to delete the values.
|
||||
*/
|
||||
async delete(names: string | string[], collection: string): Promise<void> {
|
||||
if (typeof names === 'string') {
|
||||
names = [names];
|
||||
}
|
||||
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const name = names[i];
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
localStorage.removeItem(this.getPrefixedName(name, collection));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all values for a certain collection.
|
||||
*
|
||||
* @param collection The collection to delete.
|
||||
*/
|
||||
async deleteCollection(collection: string): Promise<void> {
|
||||
const names = Object.keys(localStorage);
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const name = names[i];
|
||||
if (name.startsWith(this.getCollectionPrefix(collection))) {
|
||||
localStorage.removeItem(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -42,6 +42,8 @@ import { MediaCaptureMock } from './services/media-capture';
|
|||
import { ZipMock } from './services/zip';
|
||||
import { CorePlatform } from '@services/platform';
|
||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||
import { CoreNative } from '@features/native/services/native';
|
||||
import { SecureStorageMock } from '@features/emulator/classes/SecureStorage';
|
||||
|
||||
/**
|
||||
* This module handles the emulation of Cordova plugins in browser and desktop.
|
||||
|
@ -101,12 +103,14 @@ import { CoreLocalNotifications } from '@services/local-notifications';
|
|||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: () => () => {
|
||||
useValue: async () => {
|
||||
if (CorePlatform.is('cordova')) {
|
||||
return;
|
||||
}
|
||||
|
||||
return CoreEmulatorHelper.load();
|
||||
CoreNative.registerBrowserMock('secureStorage', new SecureStorageMock());
|
||||
|
||||
await CoreEmulatorHelper.load();
|
||||
},
|
||||
multi: true,
|
||||
},
|
||||
|
|
|
@ -24,6 +24,7 @@ import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance';
|
|||
export class CoreNativeService {
|
||||
|
||||
private plugins: Partial<Record<keyof MoodleAppPlugins, AsyncInstance>> = {};
|
||||
private mocks: Partial<Record<keyof MoodleAppPlugins, MoodleAppPlugins[keyof MoodleAppPlugins]>> = {};
|
||||
|
||||
/**
|
||||
* Get a native plugin instance.
|
||||
|
@ -31,22 +32,33 @@ export class CoreNativeService {
|
|||
* @param plugin Plugin name.
|
||||
* @returns Plugin instance.
|
||||
*/
|
||||
plugin<Plugin extends keyof MoodleAppPlugins>(plugin: Plugin): AsyncInstance<MoodleAppPlugins[Plugin]> | null {
|
||||
if (!CorePlatform.isMobile()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
plugin<Plugin extends keyof MoodleAppPlugins>(plugin: Plugin): AsyncInstance<MoodleAppPlugins[Plugin]> {
|
||||
if (!(plugin in this.plugins)) {
|
||||
this.plugins[plugin] = asyncInstance(async () => {
|
||||
await CorePlatform.ready();
|
||||
|
||||
return window.cordova?.MoodleApp?.[plugin];
|
||||
const instance = CorePlatform.isMobile() ? window.cordova?.MoodleApp?.[plugin] : this.mocks[plugin];
|
||||
if (!instance) {
|
||||
throw new Error(`Plugin ${plugin} not found.`);
|
||||
}
|
||||
|
||||
return instance;
|
||||
});
|
||||
}
|
||||
|
||||
return this.plugins[plugin] as AsyncInstance<MoodleAppPlugins[Plugin]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a mock to use in browser instead of the native plugin implementation.
|
||||
*
|
||||
* @param plugin Plugin name.
|
||||
* @param instance Instance to use.
|
||||
*/
|
||||
registerBrowserMock<Plugin extends keyof MoodleAppPlugins>(plugin: Plugin, instance: MoodleAppPlugins[Plugin]): void {
|
||||
this.mocks[plugin] = instance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const CoreNative = makeSingleton(CoreNativeService);
|
||||
|
|
Loading…
Reference in New Issue