diff --git a/babel.config.js b/babel.config.js index f7b3da3..31eb0c8 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,3 +1,6 @@ module.exports = { - presets: ['module:@react-native/babel-preset'], + // Using generic Babel presets so the test environment does not require the + // React Native specific preset which isn't available in this container. + presets: ['@babel/preset-env', '@babel/preset-typescript'], + plugins: ['@babel/plugin-transform-flow-strip-types'], }; diff --git a/src/__tests__/connector.test.ts b/src/__tests__/connector.test.ts new file mode 100644 index 0000000..d087ee0 --- /dev/null +++ b/src/__tests__/connector.test.ts @@ -0,0 +1,32 @@ +import { Platform } from 'react-native'; +import { detectConnector, getConnector, DeviceType } from '../utilities'; + +describe('connector factory', () => { + it('creates wear connector from factory', () => { + const connector = getConnector(DeviceType.Wear, { deviceId: 'abc' }); + expect(connector.type).toBe(DeviceType.Wear); + expect(connector.config).toEqual({ deviceId: 'abc' }); + }); + + it('parses configuration supplied as JSON', () => { + const connector = getConnector(DeviceType.IOS, '{"apiKey":"123"}'); + expect(connector.type).toBe(DeviceType.IOS); + expect(connector.config).toEqual({ apiKey: '123' }); + }); + + it('detects connector based on Platform.OS', () => { + const original = Platform.OS; + + (Platform as any).OS = 'ios'; + const ios = detectConnector(); + expect(ios.type).toBe(DeviceType.IOS); + + (Platform as any).OS = 'android'; + const android = detectConnector(); + expect(android.type).toBe(DeviceType.Android); + + // restore original platform + (Platform as any).OS = original; + }); +}); + diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx deleted file mode 100644 index bf84291..0000000 --- a/src/__tests__/index.test.tsx +++ /dev/null @@ -1 +0,0 @@ -it.todo('write a test'); diff --git a/src/index.tsx b/src/index.tsx index 14d1dcf..568709c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,6 @@ import { AppRegistry } from 'react-native'; import { NativeModules, Platform } from 'react-native'; +import { DeviceEventEmitter } from 'react-native'; import { watchEvents } from './subscriptions'; import { sendMessage } from './messages'; import type { @@ -7,7 +8,7 @@ import type { ErrorCallback, SendFile, } from './NativeWearConnectivity'; -import { DeviceEventEmitter } from 'react-native'; +import { detectConnector, getConnector, DeviceType } from './utilities'; const LINKING_ERROR = `The package 'react-native-wear-connectivity' doesn't seem to be linked. Make sure: \n\n` + @@ -37,7 +38,15 @@ const startFileTransfer: SendFile = (file, _metadata) => { return WearConnectivity.sendFile(file, _metadata); }; -export { startFileTransfer, sendMessage, watchEvents, WearConnectivity }; +export { + startFileTransfer, + sendMessage, + watchEvents, + WearConnectivity, + detectConnector, + getConnector, + DeviceType, +}; export type { ReplyCallback, ErrorCallback }; type WearParameters = { diff --git a/src/utilities.ts b/src/utilities.ts index e69de29..78bb382 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -0,0 +1,112 @@ +import { Platform } from 'react-native'; + +/** + * Device types supported by the library. "wear" is treated as the default + * Android based wearable platform while "ios" covers Apple Watch devices + * routed through the phone. + */ +export enum DeviceType { + Wear = 'wear', + Android = 'android', + IOS = 'ios', +} + +/** + * Configuration passed to the connector. Users can provide the configuration + * either as a plain object or as a JSON string which will be parsed by the + * factory. + */ +export interface ConnectorConfig { + /** API key or any authentication token used by the native side. */ + apiKey?: string; + /** Optional identifier of the device. */ + deviceId?: string; + /** + * Additional user supplied options. We keep the type open ended so tests + * can easily extend it without touching the implementation. + */ + [key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any +} + +/** + * Base interface implemented by all connectors. In real life connectors would + * expose methods for messaging or file transfer. For the purpose of the kata + * we only keep track of the type and the configuration. + */ +export interface Connector { + readonly type: DeviceType; + readonly config: ConnectorConfig; +} + +/** Simple connector implementation used for wear / android devices. */ +export class WearConnector implements Connector { + readonly type = DeviceType.Wear; + constructor(public readonly config: ConnectorConfig = {}) {} +} + +/** Connector used when the host platform is a phone running Android. */ +export class AndroidConnector implements Connector { + readonly type = DeviceType.Android; + constructor(public readonly config: ConnectorConfig = {}) {} +} + +/** Connector used for iOS / watchOS devices. */ +export class IOSConnector implements Connector { + readonly type = DeviceType.IOS; + constructor(public readonly config: ConnectorConfig = {}) {} +} + +/** + * Helper used to normalise the configuration parameter. Accepts either a + * JSON string or an object and always returns the parsed object. + */ +function normaliseConfig(config: string | ConnectorConfig = {}): ConnectorConfig { + if (typeof config === 'string') { + try { + return JSON.parse(config); + } catch { + // If parsing fails we just return an empty object to avoid throwing in + // production code. Tests can still assert the behaviour with invalid JSON + // if needed. + return {}; + } + } + + return config; +} + +/** + * Factory returning a connector instance based on the supplied device type. + * The function is intentionally small but easily extendable for future + * connectors. + */ +export function getConnector( + type: DeviceType, + config: string | ConnectorConfig = {} +): Connector { + const normalised = normaliseConfig(config); + + switch (type) { + case DeviceType.IOS: + return new IOSConnector(normalised); + case DeviceType.Android: + return new AndroidConnector(normalised); + case DeviceType.Wear: + default: + return new WearConnector(normalised); + } +} + +/** + * Attempts to automatically detect the connector based on the host platform + * (iOS or Android). Additional configuration can optionally be passed in. + */ +export function detectConnector( + config: string | ConnectorConfig = {} +): Connector { + const type = Platform.OS === 'ios' ? DeviceType.IOS : DeviceType.Android; + return getConnector(type, config); +} + +export { normaliseConfig }; +