Skip to content

Commit

Permalink
[final][hold] subscriber attributes (#121)
Browse files Browse the repository at this point in the history
* added skeleton methods to index.ts

* added methods to RNPurchases

* added sample attributes to the app example

* added android method calls

* updated versions of purchases-android and purchases-hybrid-common

* updated call to setAttributes to separate conversion of hashMap

* bump version numbers and purchases-hybrid-common version

* updated purchases-hybrid-common version, added hybrid additions file to xcode project

* bumped sdk version

* bump xcode

* fix xcode version

* updated simulator

* PR comments

* added test mocks and test for invalidate purchaser info cache

* added test cases to index.test.js

* Adds script to download android common files

* updates android common files

Co-authored-by: Cesar de la Vega <[email protected]>
  • Loading branch information
aboedo and vegaro authored Mar 31, 2020
1 parent 76321de commit 1555073
Show file tree
Hide file tree
Showing 15 changed files with 369 additions and 4 deletions.
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ commands:
name: Restoring iOS Build caches
- run:
command: >-
set -o pipefail && export RCT_NO_LAUNCH_PACKAGER=true && env NSUnbufferedIO=YES xcodebuild -scheme ReactNativeSample -project ./ReactNativeSample.xcodeproj -destination 'platform=iOS Simulator,name=iPhone X' -parallelizeTargets -UseModernBuildSystem=YES -derivedDataPath '~/DerivedData' | xcpretty -k
set -o pipefail && export RCT_NO_LAUNCH_PACKAGER=true && env NSUnbufferedIO=YES xcodebuild -scheme ReactNativeSample -project ./ReactNativeSample.xcodeproj -destination 'platform=iOS Simulator,name=iPhone 11 Pro' -parallelizeTargets -UseModernBuildSystem=YES -derivedDataPath '~/DerivedData' | xcpretty -k
name: Build iOS App
working_directory: example/ios
- save_cache:
Expand All @@ -76,7 +76,7 @@ executors:

ios:
macos:
xcode: 10.2.1
xcode: 11.4.0
# use a --login shell so our "set Ruby version" command gets picked up for later steps
shell: /bin/bash --login -o pipefail

Expand Down
1 change: 1 addition & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
1. Run `./scripts/download-purchases-common-android.sh 1.0.10` (change version to latest)
1. Update to the latest SDK versions in `build.js`, `RNPurchases.podspec` and `android/build.gradle`.
1. Update versions in VERSIONS.md.
1. Update version in package.json.
Expand Down
2 changes: 1 addition & 1 deletion VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
| 2.3.3 | 2.5.0 | 2.3.1 | 0.1.2 |
| 2.3.2 | 2.5.0 | 2.3.0 | 0.1.2 |
| 2.3.1 | 2.5.0 | 2.3.0 | 0.1.1 |
| 2.3.0 | 2.4.0 | 2.3.0 | N/A |
| 2.3.0 | 2.4.0 | 2.3.0 | N/A |
81 changes: 81 additions & 0 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,87 @@ describe("Purchases", () => {
expect(NativeModules.RNPurchases.purchaseProduct).toBeCalledTimes(0);
});


describe("invalidate purchaser info cache", () => {
describe("when invalidatePurchaserInfoCache is called", () => {
it("makes the right call to Purchases", () => {
const Purchases = require("../index").default;
Purchases.invalidatePurchaserInfoCache();

expect(NativeModules.RNPurchases.invalidatePurchaserInfoCache).toBeCalledTimes(1);
});
});
});

describe("setAttributes", () => {
describe("when setAttributes is called", () => {
it("makes the right call to Purchases", () => {
const Purchases = require("../index").default;
const attributes = { band: "AirBourne", song: "Back in the game" }
Purchases.setAttributes(attributes);

expect(NativeModules.RNPurchases.setAttributes).toBeCalledTimes(1);
expect(NativeModules.RNPurchases.setAttributes).toBeCalledWith(attributes);
});
});
});

describe("setEmail", () => {
describe("when setEmail is called", () => {
it("makes the right call to Purchases", () => {
const Purchases = require("../index").default;
const email = "[email protected]";

Purchases.setEmail(email);

expect(NativeModules.RNPurchases.setEmail).toBeCalledTimes(1);
expect(NativeModules.RNPurchases.setEmail).toBeCalledWith(email);
});
});
});

describe("setPhoneNumber", () => {
describe("when setPhoneNumber is called", () => {
it("makes the right call to Purchases", () => {
const Purchases = require("../index").default;
const phoneNumber = "+123456789";

Purchases.setPhoneNumber(phoneNumber);

expect(NativeModules.RNPurchases.setPhoneNumber).toBeCalledTimes(1);
expect(NativeModules.RNPurchases.setPhoneNumber).toBeCalledWith(phoneNumber);
});
});
});

describe("setDisplayName", () => {
describe("when setDisplayName is called", () => {
it("makes the right call to Purchases", () => {
const Purchases = require("../index").default;
const displayName = "Garfield";

Purchases.setDisplayName(displayName);

expect(NativeModules.RNPurchases.setDisplayName).toBeCalledTimes(1);
expect(NativeModules.RNPurchases.setDisplayName).toBeCalledWith(displayName);
});
});
});

describe("setPushToken", () => {
describe("when setPushToken is called", () => {
it("makes the right call to Purchases", () => {
const Purchases = require("../index").default;
const pushToken = "65a1ds56adsgh6954asd";

Purchases.setPushToken(pushToken);

expect(NativeModules.RNPurchases.setPushToken).toBeCalledTimes(1);
expect(NativeModules.RNPurchases.setPushToken).toBeCalledWith(pushToken);
});
});
});

const mockPlatform = OS => {
jest.resetModules();
jest.doMock("Platform", () => ({OS, select: objs => objs[OS]}));
Expand Down
1 change: 1 addition & 0 deletions android/.common_version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.0.10
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ android {
minSdkVersion safeExtGet('minSdkVersion', 16)
targetSdkVersion safeExtGet('targetSdkVersion', 26)
versionCode 1
versionName '1.0'
versionName '3.0.7'
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.json.JSONException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -204,6 +205,45 @@ public void onReceived(@NonNull PurchaserInfo purchaserInfo) {
.emit(RNPurchasesModule.PURCHASER_INFO_UPDATED, convertMapToWriteableMap(MappersKt.map((purchaserInfo))));
}

@ReactMethod
public void invalidatePurchaserInfoCache() {
CommonKt.invalidatePurchaserInfoCache();
}

//================================================================================
// Subscriber Attributes
//================================================================================

@ReactMethod
public void setAttributes(ReadableMap attributes) {
HashMap attributesHashMap = attributes.toHashMap();
CommonKt.setAttributes(attributesHashMap);
}

@ReactMethod
public void setEmail(String email) {
CommonKt.setEmail(email);
}

@ReactMethod
public void setPhoneNumber(String phoneNumber) {
CommonKt.setPhoneNumber(phoneNumber);
}

@ReactMethod
public void setDisplayName(String displayName) {
CommonKt.setDisplayName(displayName);
}

@ReactMethod
public void setPushToken(String pushToken) {
CommonKt.setPushToken(pushToken);
}

//================================================================================
// Private methods
//================================================================================

@NotNull
private OnResult getOnResult(final Promise promise) {
return new OnResult() {
Expand Down
4 changes: 4 additions & 0 deletions example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export default class App extends React.Component {
loading: false
});
}
Purchases.setPhoneNumber("123456789");
Purchases.setDisplayName("Garfield");
Purchases.setAttributes({ "favorite_cat": "garfield" });
Purchases.setEmail("[email protected]");
} catch (e) {
// eslint-disable-next-line no-console
console.log(`Error ${JSON.stringify(e)}`);
Expand Down
43 changes: 43 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,5 +722,48 @@ export default class Purchases {
* @returns { Promise<PurchasesPaymentDiscount> } Returns when the `PurchasesPaymentDiscount` is returned. Null is returned for Android and incompatible iOS versions.
*/
static getPaymentDiscount(product: PurchasesProduct, discount: PurchasesDiscount): Promise<PurchasesPaymentDiscount | undefined>;
/**
* Invalidates the cache for purchaser information.
* This is useful for cases where purchaser information might have been updated outside of the app, like if a
* promotional subscription is granted through the RevenueCat dashboard.
*/
static invalidatePurchaserInfoCache(): void;
/**
* Subscriber attributes are useful for storing additional, structured information on a user.
* Since attributes are writable using a public key they should not be used for
* managing secure or sensitive information such as subscription status, coins, etc.
*
* Key names starting with "$" are reserved names used by RevenueCat. For a full list of key
* restrictions refer to our guide: https://docs.revenuecat.com/docs/subscriber-attributes
*
* @param attributes Map of attributes by key. Set the value as an empty string to delete an attribute.
*/
static setAttributes(attributes: {
[key: string]: string | null;
}): void;
/**
* Subscriber attribute associated with the email address for the user
*
* @param email Empty String or null will delete the subscriber attribute.
*/
static setEmail(email: string | null): void;
/**
* Subscriber attribute associated with the phone number for the user
*
* @param phoneNumber Empty String or null will delete the subscriber attribute.
*/
static setPhoneNumber(phoneNumber: string | null): void;
/**
* Subscriber attribute associated with the display name for the user
*
* @param displayName Empty String or null will delete the subscriber attribute.
*/
static setDisplayName(displayName: string | null): void;
/**
* Subscriber attribute associated with the push token for the user
*
* @param pushToken null will delete the subscriber attribute.
*/
static setPushToken(pushToken: string | null): void;
}
export {};
53 changes: 53 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,59 @@ var Purchases = /** @class */ (function () {
}
return RNPurchases.getPaymentDiscount(product.identifier, discount.identifier);
};
/**
* Invalidates the cache for purchaser information.
* This is useful for cases where purchaser information might have been updated outside of the app, like if a
* promotional subscription is granted through the RevenueCat dashboard.
*/
Purchases.invalidatePurchaserInfoCache = function () {
RNPurchases.invalidatePurchaserInfoCache();
};
/**
* Subscriber attributes are useful for storing additional, structured information on a user.
* Since attributes are writable using a public key they should not be used for
* managing secure or sensitive information such as subscription status, coins, etc.
*
* Key names starting with "$" are reserved names used by RevenueCat. For a full list of key
* restrictions refer to our guide: https://docs.revenuecat.com/docs/subscriber-attributes
*
* @param attributes Map of attributes by key. Set the value as an empty string to delete an attribute.
*/
Purchases.setAttributes = function (attributes) {
RNPurchases.setAttributes(attributes);
};
/**
* Subscriber attribute associated with the email address for the user
*
* @param email Empty String or null will delete the subscriber attribute.
*/
Purchases.setEmail = function (email) {
RNPurchases.setEmail(email);
};
/**
* Subscriber attribute associated with the phone number for the user
*
* @param phoneNumber Empty String or null will delete the subscriber attribute.
*/
Purchases.setPhoneNumber = function (phoneNumber) {
RNPurchases.setPhoneNumber(phoneNumber);
};
/**
* Subscriber attribute associated with the display name for the user
*
* @param displayName Empty String or null will delete the subscriber attribute.
*/
Purchases.setDisplayName = function (displayName) {
RNPurchases.setDisplayName(displayName);
};
/**
* Subscriber attribute associated with the push token for the user
*
* @param pushToken null will delete the subscriber attribute.
*/
Purchases.setPushToken = function (pushToken) {
RNPurchases.setPushToken(pushToken);
};
/**
* Enum for attribution networks
* @readonly
Expand Down
33 changes: 33 additions & 0 deletions ios/RNPurchases.m
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,39 @@ - (dispatch_queue_t)methodQueue
completionBlock:[self getResponseCompletionBlockWithResolve:resolve reject:reject]];
}

RCT_EXPORT_METHOD(invalidatePurchaserInfoCache)
{
[RCCommonFunctionality invalidatePurchaserInfoCache];
}

#pragma mark Subscriber Attributes

RCT_EXPORT_METHOD(setAttributes:(NSDictionary *)attributes)
{
[RCCommonFunctionality setAttributes:attributes];
}

RCT_EXPORT_METHOD(setEmail:(NSString *)email)
{
[RCCommonFunctionality setEmail:email];
}

RCT_EXPORT_METHOD(setPhoneNumber:(NSString *)phoneNumber)
{
[RCCommonFunctionality setPhoneNumber:phoneNumber];
}

RCT_EXPORT_METHOD(setDisplayName:(NSString *)displayName)
{
[RCCommonFunctionality setDisplayName:displayName];
}

RCT_EXPORT_METHOD(setPushToken:(NSString *)pushToken)
{
[RCCommonFunctionality setPushToken:pushToken];
}


#pragma mark -
#pragma mark Delegate Methods
- (void)purchases:(RCPurchases *)purchases didReceiveUpdatedPurchaserInfo:(RCPurchaserInfo *)purchaserInfo {
Expand Down
2 changes: 2 additions & 0 deletions ios/RNPurchases.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
134814201AA4EA6300B7C361 /* libRNPurchases.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNPurchases.a; sourceTree = BUILT_PRODUCTS_DIR; };
35621F2A2422FA3E00053E85 /* SKProductDiscount+HybridAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SKProductDiscount+HybridAdditions.h"; sourceTree = "<group>"; };
35621F2B2422FA3E00053E85 /* SKProductDiscount+HybridAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SKProductDiscount+HybridAdditions.m"; sourceTree = "<group>"; };
2DEB4FFD242C18F100860086 /* RCPurchases+HybridAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCPurchases+HybridAdditions.h"; sourceTree = "<group>"; };
3578A04A2391AAC600E8549A /* RCCommonFunctionality.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCCommonFunctionality.h; sourceTree = "<group>"; };
3578A04B2391AAC600E8549A /* RCErrorContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCErrorContainer.m; sourceTree = "<group>"; };
3578A04C2391AAC600E8549A /* SKProduct+HybridAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SKProduct+HybridAdditions.h"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -102,6 +103,7 @@
35AB2E5F22E0134E008499FF /* Common */ = {
isa = PBXGroup;
children = (
2DEB4FFD242C18F100860086 /* RCPurchases+HybridAdditions.h */,
3578A04A2391AAC600E8549A /* RCCommonFunctionality.h */,
3578A0552391AAC600E8549A /* RCCommonFunctionality.m */,
3578A0592391AAC600E8549A /* RCEntitlementInfo+HybridAdditions.h */,
Expand Down
40 changes: 40 additions & 0 deletions scripts/download-purchases-common-android.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/sh

cd ../android/

VERSION=$1
CURRENT_VERSION=$(cat .common_version)

if [ "$VERSION" == "$CURRENT_VERSION" ]; then
echo "The newest version is already installed. Exiting."
exit 0
fi

pwd

URL=https://github.com/RevenueCat/purchases-hybrid-common/archive/$VERSION.zip

echo "Downloading Purchases common hybrid SDKs classes $VERSION from $URL, this may take a minute."

if ! which curl > /dev/null; then echo "curl command not found. Please install curl"; exit 1; fi;
if ! which unzip > /dev/null; then echo "unzip command not found. Please install unzip"; exit 1; fi;

if [[ -d ../android/src/main/java/com/revenuecat/purchases/common ]]; then
echo "Old Android classes found. Removing them and installing version $VERSION"
rm -rf ../android/src/main/java/com/revenuecat/purchases/common
fi

curl -sSL $URL > tempCommon.zip
# In some cases the temp folder can not be created by unzip, https://github.com/RevenueCat/react-native-purchases/issues/26
mkdir -p tempCommon
unzip -o tempCommon.zip -d tempCommon
ls tempCommon
mv tempCommon/purchases-hybrid-common-$VERSION/android/common/src/main/java/com/revenuecat/purchases/common/ ../android/src/main/java/com/revenuecat/purchases/
rm -r tempCommon
rm tempCommon.zip

if ! [[ -d ../android/src/main/java/com/revenuecat/purchases/common ]]; then
echo "Common files not found. Please reinstall react-native-purchases"; exit 1;
fi

echo "$VERSION" > .common_version
Loading

0 comments on commit 1555073

Please sign in to comment.