Skip to content

Commit

Permalink
Adds checkTrialOrIntroductoryPriceEligibility (RevenueCat#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
vegaro authored Dec 19, 2019
1 parent 877979b commit e2243a0
Show file tree
Hide file tree
Showing 16 changed files with 148 additions and 181 deletions.
2 changes: 1 addition & 1 deletion .common_version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.0
1.0.5
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.0.2

- Fixes issue with older versions of Kotlin (#15)
- Updates README.md
-

## 1.0.1

- Android updated to 3.0.3
Expand Down
167 changes: 1 addition & 166 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,169 +31,4 @@ To use this plugin, add `purchases_flutter` as a [dependency in your pubspec.yam
*purchases_flutter* requires XCode 10.2+ and minimum targets iOS 9.0+ and macOS 10.12+

## Getting Started
For more detailed information, you can view our complete documentation at [docs.revenuecat.com](https://docs.revenuecat.com/docs).

#### 1. Get a RevenueCat API key

Log in to the [RevenueCat dashboard](https://app.revenuecat.com) and obtain a free API key for your application.

#### 2. Initialize an `Purchases` object
> Don't forget to enable the In-App Purchase capability for your project under `Project Target -> Capabilities -> In-App Purchase`
You should only configure *Purchases* once (usually on app launch) as soon as your app has a unique user id for your user. This can be when a user logs in if you have accounts or on launch if you can generate a random user identifier.

```dart
import 'package:purchases_flutter/purchases_flutter.dart';
Purchases.setDebugLogsEnabled(true);
await Purchases.setup("my_api_key");
```

#### 3. Displaying Available Products
*Purchases* will automatically fetch the latest *active* entitlements and get the product information from Apple or Google. This means when users launch your purchase screen, products will already be loaded.

Below is an example of fetching entitlements and launching an upsell screen.

```dart
try {
Map<String, Entitlement> entitlements = await Purchases.getEntitlements();
} on PlatformException catch(e) {
}
```

#### 4. Make a purchase
When it comes time to make a purchase, *Purchases* has a simple method, `makePurchase`. The code sample below shows the process of purchasing a product and confirming it unlocks the "my_entitlement_identifier" content.

```dart
try {
PurchaserInfo purchaserInfo = await Purchases.makePurchase(product.identifier);
var isPro = purchaserInfo.entitlements.all["my_entitlement_identifier"].isActive;
if (isPro) {
// Unlock that great "pro" content
}
} on PlatformException catch (e) {
if (!(e.details as Map)["userCancelled"]) {
showError(e);
}
}
```
>`makePurchase` handles the underlying framework interaction and automatically validates purchases with Apple and Google through our secure servers. This helps reduce in-app purchase fraud and decreases the complexity of your app. Receipt tokens are stored remotely and always kept up-to-date.
#### 5. Get Subscription Status
*Purchases* makes it easy to check what active subscriptions the current user has. This can be done two ways within the `.purchaserInfo` method:
1. Checking active Entitlements - this lets you see what entitlements ([from RevenueCat dashboard](https://app.revenuecat.com)) are active for the user.
2. Checking the active subscriptions - this lets you see what product ids (from iTunes Connect or Play Store) are active for the user.

```dart
// Get purchaser info
try {
PurchaserInfo purchaserInfo = await Purchases.getPurchaserInfo();
if (purchaserInfo.entitlements.all["my_entitlement_identifier"].isActive) {
// Grant user "pro" access
}
} on PlatformException catch (e) {
// Error fetching purchaser info
}
```

>Since the SDK updates and caches the latest PurchaserInfo when the app becomes active, the completion block in `.purchaserInfo` won't need to make a network request in most cases.
##### Listening For Purchaser Info Updates
Since Purchases SDK works seamlessly on any platform, a user's purchase info may change from a variety of sources. You can respond to any changes in purchaser info by conforming to an optional listener. This will fire whenever we receive a change in purchaser info and you should expect it to be called at launch and throughout the life of the app.

Depending on your app, it may be sufficient to ignore the delegate and simply handle changes to purchaser information the next time your app is launched.

```dart
Purchases.addPurchaserInfoUpdateListener((purchaserInfo) => {
// handle any changes to purchaserInfo
});
```

### Restoring Purchases
Restoring purchases is a mechanism by which your user can restore their in-app purchases, reactivating any content that had previously been purchased from the same store account (Apple or Google).

If two different App User IDs try to restore transactions from the same underlying store account (Apple or Google) RevenueCat will create an alias between the two App User IDs and count them as the same user going forward.

This is a common if your app does not have accounts and is relying on RevenueCat's random App User IDs.

```dart
try {
PurchaserInfo purchaserInfo = Purchases.restoreTransactions();
} on PlatformException catch (e) {
// Error restoring
}
```

**Restoring purchases for logged in users:**
>If you've provided your own App User ID, calling restoreTransactions could alias the logged in user to another generated App User ID that has made a purchase on the same device.
**Allow Sharing App or Play Store Accounts**
>By default, RevenueCat will not let you reuse an App or Play Store account that already has an active subscription. If you set allowSharingAppStoreAccount = True the SDK will be permissive in accepting shared accounts, creating aliases as needed.
>By default allowSharingAppStoreAccount is True for RevenueCat random App User IDs but must be enabled manually if you want to allow permissive sharing for your own App User IDs.
## Error Handling
When an error has occurred, an `PlatformException` will be thrown. For a complete list of errors, see our online error handling documentation: https://docs.revenuecat.com/docs/errors

When investigating or logging errors, review the `details` dictionary, paying attention to the following keys:

- `readableErrorCode` contains a cross-platform error name string that can be used for identifying the error.
- `underlyingErrorMessage` contains the underlying error that caused the error in question, if an underlying error is present.

## Debugging
You can enabled detailed debug logs by setting `setDebugLogsEnabled(true)`. You can set this **before** you configure Purchases.

```dart
Purchases.setDebugLogsEnabled(true);
await Purchases.setup("my_api_key");
```

**OS_ACTIVITY_MODE**
>On iOS, disabling `OS_ACTIVITY_MODE` in your XCode scheme will block debug logs from printing in the console. If you have debug logs enabled, but don't see any output, go to `Product -> Scheme -> Edit Scheme...` in Xcode and uncheck the `OS_ACTIVITY_MODE` environment variable.
Example output:
```
[Purchases] - DEBUG: Debug logging enabled.
[Purchases] - DEBUG: SDK Version - 2.6.0
[Purchases] - DEBUG: Initial App User ID - (null)
[Purchases] - DEBUG: GET /v1/subscribers/<APP_USER_ID>
[Purchases] - DEBUG: GET /v1/subscribers/<APP_USER_ID>/products
[Purchases] - DEBUG: No cached entitlements, fetching
[Purchases] - DEBUG: Vending purchaserInfo from cache
[Purchases] - DEBUG: applicationDidBecomeActive
[Purchases] - DEBUG: GET /v1/subscribers/<APP_USER_ID>/products 200
```

## Entitlements
An entitlement represents features or content that a user is "entitled" to. With Entitlements, you can set up your available in-app products remotely and control their availability without the need to update your app. For more information on configuring entitlements, look at our [entitlements documetation](https://docs.revenuecat.com/docs/entitlements).

## Sample App
We've added an example in this project showing a simple example using *Purchases* with the RevenueCat backend. Note that the pre-registered in app purchases in the demo apps are for illustration purposes only and may not be valid in App Store Connect. [Set up your own purchases](https://docs.revenuecat.com/docs/entitlements) with RevenueCat when running the example.

## Next Steps
- Head over to our **[online documentation](https://docs.revenuecat.com/docs)** for complete setup guides
- If you haven't already, make sure your products are configured correctly in the RevenueCat dashboard by checking out our [guide on entitlements](https://docs.revenuecat.com/docs/entitlements)
- If you want to use your own user identifiers, read about [setting app user ids](https://docs.revenuecat.com/docs/user-ids)
- If you're moving to RevenueCat from another system, see our guide on [migrating your existing subscriptions](https://docs.revenuecat.com/docs/migrating-existing-subscriptions)
- Once you're ready to test your integration, you can follow our guides on [testing purchases](https://docs.revenuecat.com/docs/testing-purchases)


## Reporting Issues
You can use Github Issues to report any bugs and issues with *Purchases*. Here is some advice for users that want to report an issue:

1. Make sure that you are using the latest version of *Purchases*. The issue that you are about to report may be already fixed in the latest master branch version: https://github.com/revenuecat/purchases-flutter/tree/master.
2. Providing reproducible steps for the issue will shorten the time it takes for it to be fixed - a Gist is always welcomed!
3. Since some issues are Sandbox specific, specifying what environment you encountered the issue might help.

## Technical Support or Questions
If you have questions or need help integrating *Purchases* please start by heading to our [online documentation](https://docs.revenuecat.com/docs/welcome) and checking out the guides and support resources we have there.


## Feature Requests
If there is something you'd like to see included or feel anything is missing you can add a feature to our [public roadmap](https://trello.com/b/RZRnWRbI/revenuecat-product-roadmap). If the feature already exists, or you see something else you'd like, upvote it.


## Pricing
*Purchases* SDK is free to use but some features require a paid plan. You can find more about that on our website on the [pricing plan page](https://www.revenuecat.com/pricing).
For more detailed information, you can view our complete documentation at [docs.revenuecat.com](https://docs.revenuecat.com/docs).
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.0`
1. Run `./scripts/download-purchases-common.sh 1.0.5` (change version to latest)
1. Run `flutter format`
1. Update versions in VERSIONS.md.
1. Update version in pubspec.yaml, purchases_flutter.podspec.
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.0.2 | 3.0.1 | 3.0.4 | 1.0.5 |
| 1.0.1 | 3.0.0 | 3.0.3 | 1.0.0 |
| 1.0.0 | 3.0.0 | 3.0.2 | 1.0.0 |
| 0.3.3 | 2.6.0 | 2.4.0 | 0.1.4 |
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ android {

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.revenuecat.purchases:purchases:3.0.3'
implementation 'com.revenuecat.purchases:purchases:3.0.4'
}
14 changes: 12 additions & 2 deletions android/src/main/java/com/revenuecat/purchases/common/common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ fun purchaseProduct(
it.sku == productIdentifier && it.type.equals(type, ignoreCase = true)
}
if (productToBuy != null) {
if (oldSku.isNullOrBlank()) {
if (oldSku == null || oldSku.isBlank()) {
Purchases.sharedInstance.purchaseProductWith(
activity,
productToBuy,
Expand Down Expand Up @@ -154,7 +154,7 @@ fun purchasePackage(
it.identifier.equals(packageIdentifier, ignoreCase = true)
}
if (packageToBuy != null) {
if (oldSku.isNullOrBlank()) {
if (oldSku == null || oldSku.isBlank()) {
Purchases.sharedInstance.purchasePackageWith(
activity,
packageToBuy,
Expand Down Expand Up @@ -256,6 +256,16 @@ fun setFinishTransactions(
Purchases.sharedInstance.finishTransactions = enabled
}

// Returns Unknown for all since it's not available in Android
fun checkTrialOrIntroductoryPriceEligibility(
productIdentifiers: List<String>
): Map<String, Map<String, Any>> {
// INTRO_ELIGIBILITY_STATUS_UNKNOWN = 0
return productIdentifiers.map {
it to mapOf("status" to 0, "description" to "Status indeterminate.")
}.toMap()
}

private fun getMakePurchaseErrorFunction(onResult: OnResult): (PurchasesError, Boolean) -> Unit {
return { error, userCancelled -> onResult.onError(error.map(mapOf("userCancelled" to userCancelled))) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ private fun SkuDetails.mapIntroPrice(): Map<String, Any?> {
}

private fun String?.mapPeriod(): Map<String, Any?> {
return if (this.isNullOrBlank()) {
return if (this == null || this.isBlank()) {
mapOf(
"intro_price_period_unit" to null,
"intro_price_period_number_of_units" to null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ public void onMethodCall(MethodCall call, Result result) {
case "isAnonymous":
isAnonymous(result);
break;
case "checkTrialOrIntroductoryPriceEligibility":
productIdentifiers = call.argument("productIdentifiers");
checkTrialOrIntroductoryPriceEligibility(productIdentifiers, result);
break;
default:
result.notImplemented();
break;
Expand Down Expand Up @@ -254,6 +258,10 @@ private void isAnonymous(final Result result) {
result.success(CommonKt.isAnonymous());
}

private void checkTrialOrIntroductoryPriceEligibility(ArrayList<String> productIDs, final Result result) {
result.success(CommonKt.checkTrialOrIntroductoryPriceEligibility(productIDs));
}

@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.0)
- purchases_flutter (1.0.0):
- Purchases (3.0.1)
- purchases_flutter (1.0.2):
- Flutter
- Purchases (~> 3.0.0)
- Purchases (~> 3.0.1)

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

SPEC CHECKSUMS:
Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
Purchases: e22cbc4cd3bccdb482b5cc9a9a2a2008887d0ba4
purchases_flutter: 68053ade4aa80632afe8d3a3d29953bc0f22eae6
Purchases: f090e75880b4ee1a1aa436d469d91fd5224a70b1
purchases_flutter: a00eb27a2a857056dc92b7a358624f5d7560a08f

PODFILE CHECKSUM: 3389836f37640698630b8f0670315d626d5f1469

Expand Down
9 changes: 8 additions & 1 deletion ios/Classes/Common/RCCommonFunctionality.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ typedef void (^RCHybridResponseBlock)(NSDictionary * _Nullable, RCErrorContainer
+ (void)purchaseProduct:(NSString *)productIdentifier completionBlock:(RCHybridResponseBlock)completion;

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

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

+ (void)setFinishTransactions:(BOOL)finishTransactions;

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

@end

NS_ASSUME_NONNULL_END
NS_ASSUME_NONNULL_END
40 changes: 40 additions & 0 deletions ios/Classes/Common/RCCommonFunctionality.m
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,46 @@ + (void)purchasePackage:(NSString *)packageIdentifier offering:(NSString *)offer
}];
}

+ (void)setFinishTransactions:(BOOL)finishTransactions
{
NSAssert(RCPurchases.sharedPurchases, @"You must call setup first.");
RCPurchases.sharedPurchases.finishTransactions = finishTransactions;
}

+ (void)makeDeferredPurchase:(RCDeferredPromotionalPurchaseBlock)deferredPurchase completionBlock:(RCHybridResponseBlock)completion
{
NSAssert(RCPurchases.sharedPurchases, @"You must call setup first.");

deferredPurchase(^(SKPaymentTransaction *_Nullable transaction, RCPurchaserInfo *_Nullable purchaserInfo, NSError *_Nullable error, BOOL userCancelled) {
if (error) {
completion(nil, [self payloadForError:error withExtraPayload:@{@"userCancelled": @(userCancelled)}]);
} else {
completion(@{
@"purchaserInfo": purchaserInfo.dictionary,
@"productIdentifier": transaction.payment.productIdentifier
}, nil);
}
});
}

+ (void)checkTrialOrIntroductoryPriceEligibility:(nonnull NSArray<NSString *> *)productIdentifiers
completionBlock:(RCReceiveIntroEligibilityBlock)completion
{
NSAssert(RCPurchases.sharedPurchases, @"You must call setup first.");

[RCPurchases.sharedPurchases checkTrialOrIntroductoryPriceEligibility:productIdentifiers completionBlock:^(NSDictionary<NSString *,RCIntroEligibility *> * _Nonnull dictionary) {
NSMutableDictionary *response = [NSMutableDictionary new];
for (NSString *productID in dictionary) {
RCIntroEligibility *eligibility = dictionary[productID];
response[productID] = @{
@"status": @(eligibility.status),
@"description": eligibility.description
};
}
completion([NSDictionary dictionaryWithDictionary:response]);
}];
}

+ (void (^)(RCPurchaserInfo *, NSError *))getPurchaserInfoCompletionBlock:(RCHybridResponseBlock)completion
{
return ^(RCPurchaserInfo *_Nullable purchaserInfo, NSError *_Nullable error) {
Expand Down
10 changes: 10 additions & 0 deletions ios/Classes/PurchasesFlutterPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
[self setAutomaticAppleSearchAdsAttributionCollection:[arguments[@"enabled"] boolValue] result:result];
} else if ([@"isAnonymous" isEqualToString:call.method]) {
[self isAnonymousWithResult:result];
} else if ([@"checkTrialOrIntroductoryPriceEligibility" isEqualToString:call.method]) {
[self checkTrialOrIntroductoryPriceEligibility:arguments[@"productIdentifiers"] result:result];
} else {
result(FlutterMethodNotImplemented);
}
Expand Down Expand Up @@ -187,6 +189,14 @@ - (void)isAnonymousWithResult:(FlutterResult)result
result(@([RCCommonFunctionality isAnonymous]));
}

- (void)checkTrialOrIntroductoryPriceEligibility:(NSArray *)products
result:(FlutterResult)result
{
[RCCommonFunctionality checkTrialOrIntroductoryPriceEligibility:products completionBlock:^(NSDictionary<NSString *,RCIntroEligibility *> * _Nonnull responseDictionary) {
result([NSDictionary dictionaryWithDictionary:responseDictionary]);
}];
}

#pragma mark -
#pragma mark Delegate Methods

Expand Down
Loading

0 comments on commit e2243a0

Please sign in to comment.