diff --git a/.circleci/config.yml b/.circleci/config.yml index bd24e2b7..cd11fb8a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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: @@ -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 diff --git a/RELEASING.md b/RELEASING.md index 498cf534..81bdcd84 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -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. diff --git a/VERSIONS.md b/VERSIONS.md index 84a03a3d..a9d1b964 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -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 | \ No newline at end of file +| 2.3.0 | 2.4.0 | 2.3.0 | N/A | diff --git a/__tests__/index.test.js b/__tests__/index.test.js index faeecd10..d9d33020 100644 --- a/__tests__/index.test.js +++ b/__tests__/index.test.js @@ -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 = "garfield@revenuecat.com"; + + 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]})); diff --git a/android/.common_version b/android/.common_version new file mode 100644 index 00000000..7ee7020b --- /dev/null +++ b/android/.common_version @@ -0,0 +1 @@ +1.0.10 diff --git a/android/build.gradle b/android/build.gradle index e60a58d4..9cd15576 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,7 +26,7 @@ android { minSdkVersion safeExtGet('minSdkVersion', 16) targetSdkVersion safeExtGet('targetSdkVersion', 26) versionCode 1 - versionName '1.0' + versionName '3.0.7' } } diff --git a/android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java b/android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java index f37008b7..ae433a6a 100644 --- a/android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java +++ b/android/src/main/java/com/revenuecat/purchases/react/RNPurchasesModule.java @@ -27,6 +27,7 @@ import org.json.JSONException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -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() { diff --git a/example/App.js b/example/App.js index dcd3aece..135c6418 100644 --- a/example/App.js +++ b/example/App.js @@ -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("garfield@revenuecat.com"); } catch (e) { // eslint-disable-next-line no-console console.log(`Error ${JSON.stringify(e)}`); diff --git a/index.d.ts b/index.d.ts index 8702370c..ebba35bb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -722,5 +722,48 @@ export default class Purchases { * @returns { Promise } Returns when the `PurchasesPaymentDiscount` is returned. Null is returned for Android and incompatible iOS versions. */ static getPaymentDiscount(product: PurchasesProduct, discount: PurchasesDiscount): Promise; + /** + * 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 {}; diff --git a/index.js b/index.js index a6e9ce11..baa7274a 100644 --- a/index.js +++ b/index.js @@ -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 diff --git a/ios/RNPurchases.m b/ios/RNPurchases.m index 20f0d7ea..af50f2ba 100644 --- a/ios/RNPurchases.m +++ b/ios/RNPurchases.m @@ -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 { diff --git a/ios/RNPurchases.xcodeproj/project.pbxproj b/ios/RNPurchases.xcodeproj/project.pbxproj index 4e044e17..9554224f 100644 --- a/ios/RNPurchases.xcodeproj/project.pbxproj +++ b/ios/RNPurchases.xcodeproj/project.pbxproj @@ -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 = ""; }; 35621F2B2422FA3E00053E85 /* SKProductDiscount+HybridAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SKProductDiscount+HybridAdditions.m"; sourceTree = ""; }; + 2DEB4FFD242C18F100860086 /* RCPurchases+HybridAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCPurchases+HybridAdditions.h"; sourceTree = ""; }; 3578A04A2391AAC600E8549A /* RCCommonFunctionality.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCCommonFunctionality.h; sourceTree = ""; }; 3578A04B2391AAC600E8549A /* RCErrorContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCErrorContainer.m; sourceTree = ""; }; 3578A04C2391AAC600E8549A /* SKProduct+HybridAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SKProduct+HybridAdditions.h"; sourceTree = ""; }; @@ -102,6 +103,7 @@ 35AB2E5F22E0134E008499FF /* Common */ = { isa = PBXGroup; children = ( + 2DEB4FFD242C18F100860086 /* RCPurchases+HybridAdditions.h */, 3578A04A2391AAC600E8549A /* RCCommonFunctionality.h */, 3578A0552391AAC600E8549A /* RCCommonFunctionality.m */, 3578A0592391AAC600E8549A /* RCEntitlementInfo+HybridAdditions.h */, diff --git a/scripts/download-purchases-common-android.sh b/scripts/download-purchases-common-android.sh new file mode 100755 index 00000000..3166be4d --- /dev/null +++ b/scripts/download-purchases-common-android.sh @@ -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 diff --git a/setupJest.js b/setupJest.js index cb12e3e8..7e246f09 100644 --- a/setupJest.js +++ b/setupJest.js @@ -705,6 +705,12 @@ NativeModules.RNPurchases = { purchaseDiscountedPackage: jest.fn(), purchaseDiscountedProduct: jest.fn(), getPaymentDiscount: jest.fn(), + invalidatePurchaserInfoCache: jest.fn(), + setAttributes: jest.fn(), + setEmail: jest.fn(), + setPhoneNumber: jest.fn(), + setDisplayName: jest.fn(), + setPushToken: jest.fn(), }; jest.mock('NativeEventEmitter'); diff --git a/src/index.ts b/src/index.ts index e638b6bd..14e44137 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1007,4 +1007,65 @@ export default class Purchases { 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. + */ + public static invalidatePurchaserInfoCache() { + 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. + */ + public static setAttributes(attributes: { [key: string]: string | null }) { + RNPurchases.setAttributes(attributes); + } + + /** + * Subscriber attribute associated with the email address for the user + * + * @param email Empty String or null will delete the subscriber attribute. + */ + public static setEmail(email: string | null) { + RNPurchases.setEmail(email); + } + + /** + * Subscriber attribute associated with the phone number for the user + * + * @param phoneNumber Empty String or null will delete the subscriber attribute. + */ + public static setPhoneNumber(phoneNumber: string | null) { + RNPurchases.setPhoneNumber(phoneNumber); + } + + /** + * Subscriber attribute associated with the display name for the user + * + * @param displayName Empty String or null will delete the subscriber attribute. + */ + public static setDisplayName(displayName: string | null) { + RNPurchases.setDisplayName(displayName); + } + + /** + * Subscriber attribute associated with the push token for the user + * + * @param pushToken null will delete the subscriber attribute. + */ + public static setPushToken(pushToken: string | null) { + RNPurchases.setPushToken(pushToken); + } + }