Skip to content

Commit

Permalink
[Final] Subscriber attributes (RevenueCat#33)
Browse files Browse the repository at this point in the history
* Updates versions

* Moves result success for addAttributionData to method

* Adds Android code

* Adds iOS changes

* Adds dart changes

* Adds wip development.md

* fixes setAttributes passing wrong argument

* Updates RCCommonFunctionality

* PR comments

* Updates versions
  • Loading branch information
vegaro authored Apr 1, 2020
1 parent c6a438a commit 4b59002
Show file tree
Hide file tree
Showing 23 changed files with 688 additions and 90 deletions.
2 changes: 1 addition & 1 deletion .common_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.6
1.0.11
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.1.0

- Adds Subscriber Attributes, which allow developers to store additional, structured information
for a user in RevenueCat. More info: https://docs.revenuecat.com/docs/user-attributes.

## 1.0.5

- Updates README.md
Expand Down
3 changes: 3 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Open xcode project inside example/ios and changes made to the plugin should be automatically reflected.
- When updating iOS dependency, make sure to run `pod install` inside `example/ios/`.

2 changes: 1 addition & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
1. Update to the latest SDK versions in purchases_flutter.podspec and build.gradle.
1. Run `./scripts/download-purchases-common.sh 1.0.6` (change version to latest)
1. Run `./scripts/download-purchases-common.sh 1.0.11` (change version to latest)
1. Run `flutter format`
1. Update versions in VERSIONS.md.
1. Update version in pubspec.yaml, purchases_flutter.podspec and android/build.gradle.
Expand Down
1 change: 1 addition & 0 deletions VERSIONS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
| Version | iOS version | Android version | Common files version |
|---------|-------------|-----------------|----------------------|
| 1.1.0 | 3.2.2 | 3.1.0 | 1.0.11 |
| 1.0.5 | 3.0.1 | 3.0.4 | 1.0.6 |
| 1.0.4 | 3.0.1 | 3.0.4 | 1.0.6 |
| 1.0.3 | 3.0.1 | 3.0.4 | 1.0.6 |
Expand Down
6 changes: 3 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
group 'com.revenuecat.purchases_flutter'
version '1.0.5'
version '1.1.0'

buildscript {
ext.kotlin_version = '1.3.61'
ext.kotlin_version = '1.3.71'
repositories {
google()
jcenter()
Expand Down Expand Up @@ -38,5 +38,5 @@ android {

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.revenuecat.purchases:purchases:3.0.4'
implementation 'com.revenuecat.purchases:purchases:3.1.0'
}
28 changes: 28 additions & 0 deletions android/src/main/java/com/revenuecat/purchases/common/common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,34 @@ fun checkTrialOrIntroductoryPriceEligibility(
}.toMap()
}

fun invalidatePurchaserInfoCache() {
Purchases.sharedInstance.invalidatePurchaserInfoCache()
}

// region Subscriber Attributes

fun setAttributes(attributes: Map<String, String?>) {
Purchases.sharedInstance.setAttributes(attributes)
}

fun setEmail(email: String?) {
Purchases.sharedInstance.setEmail(email)
}

fun setPhoneNumber(phoneNumber: String?) {
Purchases.sharedInstance.setPhoneNumber(phoneNumber)
}

fun setDisplayName(displayName: String?) {
Purchases.sharedInstance.setDisplayName(displayName)
}

fun setPushToken(fcmToken: String?) {
Purchases.sharedInstance.setPushToken(fcmToken)
}

// region private functions

private fun getMakePurchaseErrorFunction(onResult: OnResult): (PurchasesError, Boolean) -> Unit {
return { error, userCancelled -> onResult.onError(error.map(mapOf("userCancelled" to userCancelled))) }
}
Expand Down
99 changes: 84 additions & 15 deletions android/src/main/java/com/revenuecat/purchases/common/mappers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.revenuecat.purchases.Offerings
import com.revenuecat.purchases.Package
import com.revenuecat.purchases.PurchaserInfo
import com.revenuecat.purchases.util.Iso8601Utils
import java.text.NumberFormat
import java.util.Currency

fun EntitlementInfo.map(): Map<String, Any?> =
mapOf(
Expand All @@ -31,16 +33,17 @@ fun EntitlementInfos.map(): Map<String, Any> =
"active" to this.active.asIterable().associate { it.key to it.value.map() }
)


fun SkuDetails.map(): Map<String, Any?> =
mapOf(
"identifier" to sku,
"description" to description,
"title" to title,
"price" to priceAmountMicros / 1000000.0,
"price_string" to price,
"currency_code" to priceCurrencyCode
) + mapIntroPrice()
"currency_code" to priceCurrencyCode,
"introPrice" to mapIntroPrice(),
"discounts" to null
) + mapIntroPriceDeprecated()

fun PurchaserInfo.map(): Map<String, Any?> =
mapOf(
Expand Down Expand Up @@ -90,28 +93,24 @@ private fun Package.map(offeringIdentifier: String): Map<String, Any?> =
"offeringIdentifier" to offeringIdentifier
)

private fun SkuDetails.mapIntroPrice(): Map<String, Any?> {
return if (!freeTrialPeriod.isNullOrBlank()) {
// Check freeTrialPeriod first to give priority to trials
// Format using device locale. iOS will format using App Store locale, but there's no way
// to figure out how the price in the SKUDetails is being formatted.
val format = java.text.NumberFormat.getCurrencyInstance().apply {
currency = java.util.Currency.getInstance(priceCurrencyCode)
}
private fun SkuDetails.mapIntroPriceDeprecated(): Map<String, Any?> {
val isFreeTrialAvailable = freeTrialPeriod != null && freeTrialPeriod.isNotBlank()
return if (isFreeTrialAvailable) {
val format = formatUsingDeviceLocale()
mapOf(
"intro_price" to 0,
"intro_price_string" to format.format(0),
"intro_price_period" to freeTrialPeriod,
"intro_price_cycles" to 1
) + freeTrialPeriod.mapPeriod()
} else if (!introductoryPrice.isNullOrBlank()) {
) + freeTrialPeriod.mapPeriodDeprecated()
} else if (introductoryPrice != null || introductoryPrice.isNotBlank()) {
mapOf(
"intro_price" to introductoryPriceAmountMicros / 1000000.0,
"intro_price_string" to introductoryPrice,
"intro_price_period" to introductoryPricePeriod,
"intro_price_cycles" to (introductoryPriceCycles?.takeUnless { it.isBlank() }?.toInt()
?: 0)
) + introductoryPricePeriod.mapPeriod()
) + introductoryPricePeriod.mapPeriodDeprecated()
} else {
mapOf(
"intro_price" to null,
Expand All @@ -124,7 +123,47 @@ private fun SkuDetails.mapIntroPrice(): Map<String, Any?> {
}
}

private fun String?.mapPeriod(): Map<String, Any?> {
private fun SkuDetails.formatUsingDeviceLocale(): NumberFormat {
return NumberFormat.getCurrencyInstance().apply {
currency = Currency.getInstance(priceCurrencyCode)
}
}

private fun SkuDetails.mapIntroPrice(): Map<String, Any?> {
return if (freeTrialPeriod != null && freeTrialPeriod.isNotBlank()) {
// Check freeTrialPeriod first to give priority to trials
// Format using device locale. iOS will format using App Store locale, but there's no way
// to figure out how the price in the SKUDetails is being formatted.
val format = java.text.NumberFormat.getCurrencyInstance().apply {
currency = java.util.Currency.getInstance(priceCurrencyCode)
}
mapOf(
"price" to 0,
"priceString" to format.format(0),
"period" to freeTrialPeriod,
"cycles" to 1
) + freeTrialPeriod.mapPeriod()
} else if (introductoryPrice != null && introductoryPrice.isNotBlank()) {
mapOf(
"price" to introductoryPriceAmountMicros / 1000000.0,
"priceString" to introductoryPrice,
"period" to introductoryPricePeriod,
"cycles" to (introductoryPriceCycles?.takeUnless { it.isBlank() }?.toInt()
?: 0)
) + introductoryPricePeriod.mapPeriod()
} else {
mapOf(
"price" to null,
"priceString" to null,
"period" to null,
"cycles" to null,
"periodUnit" to null,
"periodNumberOfUnits" to null
)
}
}

private fun String?.mapPeriodDeprecated(): Map<String, Any?> {
return if (this == null || this.isBlank()) {
mapOf(
"intro_price_period_unit" to null,
Expand Down Expand Up @@ -153,3 +192,33 @@ private fun String?.mapPeriod(): Map<String, Any?> {
}
}
}

private fun String?.mapPeriod(): Map<String, Any?> {
return if (this == null || this.isBlank()) {
mapOf(
"periodUnit" to null,
"periodNumberOfUnits" to null
)
} else {
PurchasesPeriod.parse(this).let { period ->
when {
period.years > 0 -> mapOf(
"periodUnit" to "YEAR",
"periodNumberOfUnits" to period.years
)
period.months > 0 -> mapOf(
"periodUnit" to "MONTH",
"periodNumberOfUnits" to period.months
)
period.days > 0 -> mapOf(
"periodUnit" to "DAY",
"periodNumberOfUnits" to period.days
)
else -> mapOf(
"periodUnit" to "DAY",
"periodNumberOfUnits" to 0
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ public void onMethodCall(MethodCall call, Result result) {
Map<String, String> data = call.argument("data");
int network = call.argument("network") != null ? (int) call.argument("network") : -1;
String networkUserId = call.argument("networkUserId");
addAttributionData(data, network, networkUserId);
result.success(null);
addAttributionData(data, network, networkUserId, result);
break;
case "getOfferings":
getOfferings(result);
Expand Down Expand Up @@ -140,6 +139,29 @@ public void onMethodCall(MethodCall call, Result result) {
productIdentifiers = call.argument("productIdentifiers");
checkTrialOrIntroductoryPriceEligibility(productIdentifiers, result);
break;
case "invalidatePurchaserInfoCache":
invalidatePurchaserInfoCache(result);
break;
case "setAttributes":
Map<String, String> attributes = call.argument("attributes");
setAttributes(attributes, result);
break;
case "setEmail":
String email = call.argument("email");
setEmail(email, result);
break;
case "setPhoneNumber":
String phoneNumber = call.argument("phoneNumber");
setPhoneNumber(phoneNumber, result);
break;
case "setDisplayName":
String displayName = call.argument("displayName");
setDisplayName(displayName, result);
break;
case "setPushToken":
String pushToken = call.argument("pushToken");
setPushToken(pushToken, result);
break;
default:
result.notImplemented();
break;
Expand Down Expand Up @@ -170,8 +192,10 @@ private void setAllowSharingAppStoreAccount(boolean allowSharingAppStoreAccount,
result.success(null);
}

private void addAttributionData(Map<String, String> data, int network, @Nullable String networkUserId) {
private void addAttributionData(Map<String, String> data, int network,
@Nullable String networkUserId, Result result) {
CommonKt.addAttributionData(data, network, networkUserId);
result.success(null);
}

private void getOfferings(final Result result) {
Expand Down Expand Up @@ -263,6 +287,40 @@ private void checkTrialOrIntroductoryPriceEligibility(ArrayList<String> productI
result.success(CommonKt.checkTrialOrIntroductoryPriceEligibility(productIDs));
}

private void invalidatePurchaserInfoCache(Result result) {
CommonKt.invalidatePurchaserInfoCache();
result.success(null);
}

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

private void setAttributes(Map<String, String> map, final Result result) {
CommonKt.setAttributes(map);
result.success(null);
}

private void setEmail(String email, final Result result) {
CommonKt.setEmail(email);
result.success(null);
}

private void setPhoneNumber(String phoneNumber, final Result result) {
CommonKt.setPhoneNumber(phoneNumber);
result.success(null);
}

private void setDisplayName(String displayName, final Result result) {
CommonKt.setDisplayName(displayName);
result.success(null);
}

private void setPushToken(String pushToken, final Result result) {
CommonKt.setPushToken(pushToken);
result.success(null);
}

@NotNull
private OnResult getOnResult(final Result result) {
return new OnResult() {
Expand Down
10 changes: 5 additions & 5 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
PODS:
- Flutter (1.0.0)
- Purchases (3.0.1)
- purchases_flutter (1.0.3):
- Purchases (3.2.2)
- purchases_flutter (1.1.0):
- Flutter
- Purchases (~> 3.0.1)
- Purchases (~> 3.2.2)

DEPENDENCIES:
- Flutter (from `.symlinks/flutter/ios`)
Expand All @@ -21,8 +21,8 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
Purchases: f090e75880b4ee1a1aa436d469d91fd5224a70b1
purchases_flutter: 34a610c5c72ead858c93af15f9e189b1b516b498
Purchases: 17c95d62be02c876c56f6fe6b34fda2016a09063
purchases_flutter: ba6f93f4e355fe67f4074fa2bf668cc4ab056dfd

PODFILE CHECKSUM: 3389836f37640698630b8f0670315d626d5f1469

Expand Down
22 changes: 19 additions & 3 deletions ios/Classes/Common/RCCommonFunctionality.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ typedef void (^RCHybridResponseBlock)(NSDictionary * _Nullable, RCErrorContainer

@interface RCCommonFunctionality : NSObject

+ (void)configure;

+ (void)setAllowSharingStoreAccount:(BOOL)allowSharingStoreAccount;

+ (void)addAttributionData:(NSDictionary *)data network:(NSInteger)network networkUserId:(NSString *)networkUserId;
Expand All @@ -39,15 +41,29 @@ typedef void (^RCHybridResponseBlock)(NSDictionary * _Nullable, RCErrorContainer

+ (BOOL)isAnonymous;

+ (void)purchaseProduct:(NSString *)productIdentifier completionBlock:(RCHybridResponseBlock)completion;
+ (void)purchaseProduct:(NSString *)productIdentifier signedDiscountTimestamp:(nullable NSString *)discountTimestamp completionBlock:(RCHybridResponseBlock)completion;

+ (void)purchasePackage:(NSString *)packageIdentifier offering:(NSString *)offeringIdentifier completionBlock:(RCHybridResponseBlock)completion;
+ (void)purchasePackage:(NSString *)packageIdentifier offering:(NSString *)offeringIdentifier signedDiscountTimestamp:(nullable NSString *)discountTimestamp completionBlock:(RCHybridResponseBlock)completion;

+ (void)makeDeferredPurchase:(RCDeferredPromotionalPurchaseBlock)deferredPurchase completionBlock:(RCHybridResponseBlock)completion;

+ (void)setFinishTransactions:(BOOL)finishTransactions;

+ (void)checkTrialOrIntroductoryPriceEligibility:(nonnull NSArray<NSString *> *)productIdentifiers completionBlock:(void (^)(NSDictionary<NSString *, NSDictionary *> *))completion;
+ (void)checkTrialOrIntroductoryPriceEligibility:(nonnull NSArray<NSString *> *)productIdentifiers completionBlock:(RCReceiveIntroEligibilityBlock)completion;

+ (void)paymentDiscountForProductIdentifier:(NSString *)productIdentifier discount:(nullable NSString *)discountIdentifier completionBlock:(RCHybridResponseBlock)completion;

+ (void)invalidatePurchaserInfoCache;

+ (void)setAttributes:(NSDictionary<NSString *, NSString *> *)attributes;

+ (void)setEmail:(nullable NSString *)email;

+ (void)setPhoneNumber:(nullable NSString *)phoneNumber;

+ (void)setDisplayName:(nullable NSString *)displayName;

+ (void)setPushToken:(nullable NSString *)pushToken;

@end

Expand Down
Loading

0 comments on commit 4b59002

Please sign in to comment.