Skip to content

feat: Allow brownfield iOS apps to handle storage. #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ getData = async () => {

```

See docs for [api and more examples.](docs/API.md)
See docs for [api and more examples](docs/API.md), and [brownfield integration guide](docs/AdvancedUsage.md).

## Writing tests

Expand All @@ -66,4 +66,4 @@ See the [CONTRIBUTING](CONTRIBUTING.md) file for how to help out.

## License

MIT
MIT
87 changes: 87 additions & 0 deletions docs/AdvancedUsage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Integrating Async Storage with embedded React Native apps

If you're embedding React Native into native application, you can also integrate
Async Storage module, so that both worlds will use one storage solution.

## iOS

AsyncStorage can be controlled by the hosting app via the delegate on
`RNCAsyncStorage`:

```objc
RNCAsyncStorage *asyncStorage = [bridge moduleForClass:[RNCAsyncStorage class]];
asyncStorage.delegate = self;
```

### The procotol

The delegate must conform to the `RNCAsyncStorageDelegate` protocol

---

```objc
- (void)allKeys:(RNCAsyncStorageResultCallback)block;
```

Returns all keys currently stored. If none, an empty array is returned.
Called by `getAllKeys` in JS.

---

```objc
- (void)mergeValues:(NSArray<NSString *> *)values
forKeys:(NSArray<NSString *> *)keys
completion:(RNCAsyncStorageResultCallback)block;
```

Merges values with the corresponding values stored at specified keys.
Called by `mergeItem` and `multiMerge` in JS.

---

```objc
- (void)removeAllValues:(RNCAsyncStorageCompletion)block;
```

Removes all values from the store. Called by `clear` in JS.

---

```objc
- (void)removeValuesForKeys:(NSArray<NSString *> *)keys
completion:(RNCAsyncStorageResultCallback)block;
```

Removes all values associated with specified keys.
Called by `removeItem` and `multiRemove` in JS.

---

```objc
- (void)setValues:(NSArray<NSString *> *)values
forKeys:(NSArray<NSString *> *)keys
completion:(RNCAsyncStorageResultCallback)block;
```

Sets specified key-value pairs. Called by `setItem` and `multiSet` in JS.

---

```objc
- (void)valuesForKeys:(NSArray<NSString *> *)keys
completion:(RNCAsyncStorageResultCallback)block;
```

Returns values associated with specified keys.
Called by `getItem` and `multiGet` in JS.

---

```objc
@optional
@property (nonatomic, readonly, getter=isPassthrough) BOOL passthrough;
```

**Optional:** Returns whether the delegate should be treated as a passthrough.
This is useful for monitoring storage usage among other things. Returns `NO` by
default.
138 changes: 136 additions & 2 deletions example/e2e/asyncstorage.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,19 @@ describe('Async Storage', () => {
});

describe('get / set / clear item test', () => {
it('should be visible', async () => {
beforeAll(async () => {
if (device.getPlatform() === 'ios') {
await device.openURL({url: 'rnc-asyncstorage://unset-delegate'});
}
await test_getSetClear.tap();
});

it('should be visible', async () => {
await expect(element(by.id('clear_button'))).toExist();
await expect(element(by.id('increaseByTen_button'))).toExist();
await expect(element(by.id('storedNumber_text'))).toExist();
});

it('should store value in async storage', async () => {
const storedNumberText = await element(by.id('storedNumber_text'));
const increaseByTenButton = await element(by.id('increaseByTen_button'));
Expand Down Expand Up @@ -65,8 +72,14 @@ describe('Async Storage', () => {
});

describe('merge item test', () => {
it('should be visible', async () => {
beforeAll(async () => {
if (device.getPlatform() === 'ios') {
await device.openURL({url: 'rnc-asyncstorage://unset-delegate'});
}
await test_mergeItem.tap();
});

it('should be visible', async () => {
await expect(element(by.id('saveItem_button'))).toExist();
await expect(element(by.id('mergeItem_button'))).toExist();
await expect(element(by.id('restoreItem_button'))).toExist();
Expand Down Expand Up @@ -139,4 +152,125 @@ describe('Async Storage', () => {
expect(storyText).toHaveText(newStory);
});
});

describe('get / set / clear item delegate test', () => {
beforeAll(async () => {
await test_getSetClear.tap();
if (device.getPlatform() === 'android') {
// Not yet supported.
return;
}

await device.openURL({url: 'rnc-asyncstorage://set-delegate'});
});

it('should store value with delegate', async () => {
if (device.getPlatform() === 'android') {
// Not yet supported.
return;
}

const storedNumberText = await element(by.id('storedNumber_text'));
const increaseByTenButton = await element(by.id('increaseByTen_button'));

await expect(storedNumberText).toHaveText('');

const tapTimes = Math.round(Math.random() * 9) + 1;

for (let i = 0; i < tapTimes; i++) {
await increaseByTenButton.tap();
}

await expect(storedNumberText).toHaveText(`${tapTimes * 10}`);
await restartButton.tap();

// The delegate will distinguish itself by always returning the stored value + 1000000
await expect(storedNumberText).toHaveText(`${tapTimes * 10 + 1000000}`);
});

it('should clear item with delegate', async () => {
if (device.getPlatform() === 'android') {
// Not yet supported.
return;
}

const storedNumberText = await element(by.id('storedNumber_text'));
const increaseByTenButton = await element(by.id('increaseByTen_button'));
const clearButton = await element(by.id('clear_button'));

await increaseByTenButton.tap();
await clearButton.tap();
await restartButton.tap();

// The delegate will distinguish itself by actually setting storing 1000000
// instead of clearing. It will also always return the stored value + 1000000.
await expect(storedNumberText).toHaveText('2000000');
});
});

describe('merge item delegate test', () => {
beforeAll(async () => {
if (device.getPlatform() === 'ios') {
await device.openURL({url: 'rnc-asyncstorage://set-delegate'});
}
await test_mergeItem.tap();
});

it('should merge items with delegate', async () => {
if (device.getPlatform() === 'android') {
// Not yet supported.
return;
}

const buttonMergeItem = await element(by.id('mergeItem_button'));
const buttonRestoreItem = await element(by.id('restoreItem_button'));

const nameInput = await element(by.id('testInput-name'));
const ageInput = await element(by.id('testInput-age'));
const eyesInput = await element(by.id('testInput-eyes'));
const shoeInput = await element(by.id('testInput-shoe'));
const storyText = await element(by.id('storyTextView'));

const isAndroid = device.getPlatform() === 'android';

async function performInput() {
const name = Math.random() > 0.5 ? 'Jerry' : 'Sarah';
const age = Math.random() > 0.5 ? '21' : '23';
const eyesColor = Math.random() > 0.5 ? 'blue' : 'green';
const shoeSize = Math.random() > 0.5 ? '9' : '10';

if (!isAndroid) {
await eyesInput.tap();
}
await nameInput.typeText(name);
await closeKeyboard.tap();

if (!isAndroid) {
await eyesInput.tap();
}
await ageInput.typeText(age);
await closeKeyboard.tap();

if (!isAndroid) {
await eyesInput.tap();
}
await eyesInput.typeText(eyesColor);
await closeKeyboard.tap();

if (!isAndroid) {
await eyesInput.tap();
}
await shoeInput.typeText(shoeSize);
await closeKeyboard.tap();

return `${name} from delegate is ${age} from delegate, has ${eyesColor} eyes and shoe size of ${shoeSize}.`;
}

const story = await performInput();
await buttonMergeItem.tap();
await restartButton.tap();
await buttonRestoreItem.tap();
expect(storyText).toHaveText(story);
});
});
});
38 changes: 33 additions & 5 deletions example/ios/AsyncStorageExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; };
196F5D682254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */; };
19C469542256303E00CA1332 /* RNCTestAsyncStorageDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 19C469532256303E00CA1332 /* RNCTestAsyncStorageDelegate.m */; };
3D82E3B72248BD39001F5D1A /* libRNCAsyncStorage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DC5398C220F2C940035D3A3 /* libRNCAsyncStorage.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBDB9271DFEBF0700ED6528 /* libRCTBlob.a */; };
Expand Down Expand Up @@ -84,6 +86,13 @@
remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192;
remoteInfo = React;
};
1990B95122398FC4009E5EA1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 3DC5395A220F2C940035D3A3 /* RNCAsyncStorage.xcodeproj */;
proxyType = 1;
remoteGlobalIDString = 58B511DA1A9E6C8500147676;
remoteInfo = RNCAsyncStorage;
};
2D16E6711FA4F8DC00B85C8A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = ADBDB91F1DFEBF0600ED6528 /* RCTBlob.xcodeproj */;
Expand Down Expand Up @@ -313,6 +322,10 @@
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = AsyncStorageExample/Info.plist; sourceTree = "<group>"; };
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = AsyncStorageExample/main.m; sourceTree = "<group>"; };
146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../../node_modules/react-native/React/React.xcodeproj"; sourceTree = "<group>"; };
196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "AppDelegate+RNCAsyncStorageDelegate.m"; path = "AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.m"; sourceTree = "<group>"; };
196F5D8E2254C2C90035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "AppDelegate+RNCAsyncStorageDelegate.h"; path = "AsyncStorageExample/AppDelegate+RNCAsyncStorageDelegate.h"; sourceTree = "<group>"; };
19C4692D22562FD400CA1332 /* RNCTestAsyncStorageDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = RNCTestAsyncStorageDelegate.h; path = AsyncStorageExample/RNCTestAsyncStorageDelegate.h; sourceTree = "<group>"; };
19C469532256303E00CA1332 /* RNCTestAsyncStorageDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = RNCTestAsyncStorageDelegate.m; path = AsyncStorageExample/RNCTestAsyncStorageDelegate.m; sourceTree = "<group>"; };
2D16E6891FA4F8E400B85C8A /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; };
3DC5395A220F2C940035D3A3 /* RNCAsyncStorage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNCAsyncStorage.xcodeproj; path = ../../ios/RNCAsyncStorage.xcodeproj; sourceTree = "<group>"; };
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = "<group>"; };
Expand All @@ -329,11 +342,9 @@
buildActionMask = 2147483647;
files = (
3D82E3B72248BD39001F5D1A /* libRNCAsyncStorage.a in Frameworks */,
ED297163215061F000B7C4FE /* JavaScriptCore.framework in Frameworks */,
ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */,
11D1A2F320CAFA9E000508D9 /* libRCTAnimation.a in Frameworks */,
146834051AC3E58100842450 /* libReact.a in Frameworks */,
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */,
11D1A2F320CAFA9E000508D9 /* libRCTAnimation.a in Frameworks */,
ADBDB9381DFEBF1600ED6528 /* libRCTBlob.a in Frameworks */,
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */,
00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */,
133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */,
Expand All @@ -342,6 +353,8 @@
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */,
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
146834051AC3E58100842450 /* libReact.a in Frameworks */,
ED297163215061F000B7C4FE /* JavaScriptCore.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -416,6 +429,10 @@
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
13B07FB01A68108700A75B9A /* AppDelegate.m */,
196F5D8E2254C2C90035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.h */,
196F5D672254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m */,
19C4692D22562FD400CA1332 /* RNCTestAsyncStorageDelegate.h */,
19C469532256303E00CA1332 /* RNCTestAsyncStorageDelegate.m */,
13B07FB51A68108700A75B9A /* Images.xcassets */,
13B07FB61A68108700A75B9A /* Info.plist */,
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
Expand Down Expand Up @@ -557,6 +574,7 @@
buildRules = (
);
dependencies = (
1990B95222398FC4009E5EA1 /* PBXTargetDependency */,
);
name = AsyncStorageExample;
productName = "Hello World";
Expand Down Expand Up @@ -938,7 +956,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "export NODE_BINARY=node\n../../node_modules/react-native/scripts/react-native-xcode.sh";
shellScript = "export NODE_BINARY=node\n../../node_modules/react-native/scripts/react-native-xcode.sh\n";
};
/* End PBXShellScriptBuildPhase section */

Expand All @@ -948,12 +966,22 @@
buildActionMask = 2147483647;
files = (
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */,
19C469542256303E00CA1332 /* RNCTestAsyncStorageDelegate.m in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
196F5D682254C1530035A6D3 /* AppDelegate+RNCAsyncStorageDelegate.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */

/* Begin PBXTargetDependency section */
1990B95222398FC4009E5EA1 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = RNCAsyncStorage;
targetProxy = 1990B95122398FC4009E5EA1 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */

/* Begin PBXVariantGroup section */
13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
isa = PBXVariantGroup;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import "AppDelegate.h"

#import <RNCAsyncStorage/RNCAsyncStorageDelegate.h>

@interface AppDelegate (RNCAsyncStorageDelegate) <RNCAsyncStorageDelegate>
@end
Loading