Skip to content

Commit

Permalink
Merge pull request #255 from dpa99c/master
Browse files Browse the repository at this point in the history
Apple-hosted downloadable content
  • Loading branch information
dpa99c committed Aug 5, 2015
2 parents 4915a9d + f2806f3 commit c9044ea
Show file tree
Hide file tree
Showing 20 changed files with 828 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
*~

npm-debug.log
/.idea
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ You're all good? Time to read some more documentation. Hooray!
* Guillaume Charhon (initial Android code)
* Matt Kane (initial iOS code)
* Mohammad Naghavi (original unification attempt)
* Dave Alden [@dpa99c](https://github.com/dpa99c)(Apple-hosted IAPs for iOS)

## Sponsors
Big thanks to:
Expand Down
22 changes: 19 additions & 3 deletions doc/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ be notified of changes to one or a set of products using a [`query`](#queries) m
store.when("product").updated(refreshScreen);
store.when("full version").owned(unlockApp);
store.when("subscription").approved(serverCheck);
store.when("downloadable content").downloaded(showContent);
etc.
```

Expand Down Expand Up @@ -292,6 +293,7 @@ See the [logging levels](#logging-levels) constants.
store.ERR_BAD_RESPONSE = ERROR_CODES_BASE + 18; // Verification of store data failed.
store.ERR_REFRESH = ERROR_CODES_BASE + 19; // Failed to refresh the store.
store.ERR_PAYMENT_EXPIRED = ERROR_CODES_BASE + 20;
store.ERR_DOWNLOAD = ERROR_CODES_BASE + 21;

### product states

Expand All @@ -303,6 +305,8 @@ See the [logging levels](#logging-levels) constants.
store.APPROVED = 'approved';
store.FINISHED = 'finished';
store.OWNED = 'owned';
store.DOWNLOADING = 'downloading';
store.DOWNLOADED = 'downloaded';

### logging levels

Expand Down Expand Up @@ -337,6 +341,8 @@ Products object have the following fields and methods.
- `product.valid` - Product has been loaded and is a valid product
- `product.canPurchase` - Product is in a state where it can be purchased
- `product.owned` - Product is owned
- `product.downloading` - Product is downloading non-consumable content
- `product.downloaded` - Non-consumable content has been successfully downloaded for this product
- `product.transaction` - Latest transaction data for this product (see [transactions](#transactions)).

### *store.Product* public methods
Expand Down Expand Up @@ -403,9 +409,11 @@ Find below a diagram of the different states a product can pass by.
|
^ +------------------------------+
| |
| +--> APPROVED +--> FINISHED +--> OWNED
| |
+----------------------------------+
| | +--> DOWNLOADING +--> DOWNLOADED +
| | | |
| +--> APPROVED +--------------------------------+--> FINISHED +--> OWNED
| |
+-------------------------------------------------------------+

#### states definitions

Expand All @@ -417,6 +425,8 @@ Find below a diagram of the different states a product can pass by.
- `APPROVED`: purchase approved by server
- `FINISHED`: purchase delivered by the app (see [Finish a Purchase](#finish-a-purchase))
- `OWNED`: purchase is owned (only for non-consumable and subscriptions)
- `DOWNLOADING` purchased content is downloading (only for non-consumable)
- `DOWNLOADED` purchased content is downloaded (only for non-consumable)

#### Notes

Expand Down Expand Up @@ -503,6 +513,8 @@ Some reserved keywords can't be used in the product `id` and `alias`:
- `approved`
- `owned`
- `finished`
- `downloading`
- `downloaded`
- `refreshed`

## <a name="get"></a>*store.get(id)*
Expand Down Expand Up @@ -549,6 +561,10 @@ product events defined below.
- Called when receipt verification failed
- `expired(product)`
- Called when validation find a subscription to be expired
- `downloading(product, progress, time_remaining)`
- Called when content download is started
- `downloaded(product)`
- Called when content download has successfully completed

### alternative usage

Expand Down
15 changes: 15 additions & 0 deletions doc/contributor-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ A non-consumable product, once `OWNED` always will be.

http://stackoverflow.com/questions/6429186/can-we-check-if-a-users-in-app-purchase-has-been-refunded-by-apple

#### persist downloaded status

`storekit` doesn't provide a way to know which products have been downloaded.
That is why we have to handle that ourselves, by storing the `DOWNLOADED` status of a product.

A non-consumable product, once `OWNED` can always be re-downloaded for free.


## Initialization

Expand Down Expand Up @@ -114,6 +121,14 @@ during this or a previous execution of the application.
#### *setOwned(productId, value)*
store the boolean OWNED status of a given product.

## Persistance of the *DOWNLOADED* status

#### *isDownloaded(productId)*
return true if the product with given ID has been purchased and finished downloading
during this or a previous execution of the application.
#### *setDownloaded(productId, value)*
store the boolean DOWNLOADED status of a given product.

## Retry failed requests
When setup and/or load failed, the plugin will retry over and over till it can connect
to the store.
Expand Down
38 changes: 38 additions & 0 deletions doc/ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,41 @@ However even when repurchasing the same subscription it will NOT auto-renew
again on the same test account since it has already auto-renewed 5 times.
So if you want to test renewal and you have been messing with these
subscriptions for a while you need to create a new itunes connect test user.


### Hosted content

Apple offers the option to host non-consumable content on its servers, which is automatically downloaded to the device on successfully purchasing a non-consumable IAP (see the [documentation](https://developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnectInAppPurchase_Guide/Chapters/CreatingInAppPurchaseProducts.html#//apple_ref/doc/uid/TP40013727-CH3-SW4) in the Apple Dev Center for more on this).

To configure this in the demo app, follow these additional steps:

iTunes Connect

- Create a new purchase in iTunes Connect (type non-consumable)
- (Add a dummy screenshot)
- Check the box "Hosting Content with Apple"

Demo IAP content project

- Clone this Git repo containing a demo IAP containing content for hosting: [https://github.com/dpa99c/cordova-plugin-purchase-demo-ios-hosted](https://github.com/dpa99c/cordova-plugin-purchase-demo-ios-hosted)
- Edit the ContentInfo.plist (either in XCode or text editor) and set the `IAPProductIdentifier` key appropriately for your app Identifier
- Using XCode, select from the menu "Product" > "Archive"
- Then "Export..." > "Export as an Installer Package" > "Next" > "Export" to create an IAP .pkg file

Application Loader

- Download and install Apple's [Application Loader](https://itunesconnect.apple.com/docs/UsingApplicationLoader.pdf)
- Run Application Loaded and sign in with your iTunes Connect account details
- On the "Template Chooser" screen, select "New In-App purchases" > "Choose"
- Select the demo app Identifier > "Manage"
- Select the non-consumable IAP configured for Hosted content that you created above
- Select "Hosted Content"
- Ensure "Host Content with Apple" is checked and select "Choose..."
- Browse to and select the IAP .pkg file you exported from XCode
- Select "Next", then "Save" if prompted, then "Deliver"
- Once the package is uploaded to iTunes Connect, you'll see a big green tick

Demo application project

- Edit [config.xml](https://github.com/dpa99c/cordova-plugin-purchase-demo/blob/master/config.xml) and set the `id` attribute in the `<widget>` element to that of your app Identifier
- Edit [www/index.js](https://github.com/dpa99c/cordova-plugin-purchase-demo/blob/master/www/js/index.js) and set the `id` fields under `store.register` are for your IAP Identifiers.
2 changes: 2 additions & 0 deletions plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ SOFTWARE.
<source-file src="src/ios/InAppPurchase.m" />
<header-file src="src/ios/SKProduct+LocalizedPrice.h" />
<source-file src="src/ios/SKProduct+LocalizedPrice.m" />
<header-file src="src/ios/FileUtility.h" />
<source-file src="src/ios/FileUtility.m" />

<framework src="StoreKit.framework" />
</platform>
Expand Down
25 changes: 25 additions & 0 deletions src/ios/FileUtility.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@interface FileUtility : NSObject
{
// Static Class
}



#pragma mark Files & Paths

+(BOOL) copyToDocuments:(NSString *)file;

+(NSArray *) listDocs:(NSString *)extension;
+(NSArray *) listFiles:(NSString *)path extension:(NSString *)extension;

+(NSString *) getDocumentPath;
+(BOOL) isDocumentExist:(NSString *)file;
+(BOOL) isFileExist:(NSString *)path;
+(BOOL) isFolderExist:(NSString *)path;
+(NSString *) getAppendDocPath:(NSString *)file;
+(BOOL) createFolder:(NSString *)folder;
+(BOOL) copyFile:(NSString *)src dst:(NSString *)dst;

#pragma end

@end
91 changes: 91 additions & 0 deletions src/ios/FileUtility.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#import <sys/utsname.h>
#import "FileUtility.h"

@implementation FileUtility

/****************************************************************************************************************
* File and folder utilities
****************************************************************************************************************/
#pragma mark Files & Paths

+(NSArray *) listFiles:(NSString *)path extension:(NSString *)extension
{
NSMutableArray *files = [[NSMutableArray alloc] init];

for (NSString *file in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil])
{
if (extension == nil || [[file pathExtension] isEqualToString:extension])
{
[files addObject:file];
}
}

return files;
}

+(NSArray *) listDocs:(NSString *)extension
{
return [FileUtility listFiles:[FileUtility getDocumentPath] extension:extension];
}

+(NSString *) getAppendDocPath:(NSString *)file
{
return [[FileUtility getDocumentPath] stringByAppendingPathComponent:file];
}

+(NSString *) getDocumentPath
{
return [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
}


+(BOOL) isDocumentExist:(NSString *)file
{
return [FileUtility isFileExist:[[FileUtility getDocumentPath] stringByAppendingPathComponent:file]];
}

+(BOOL) isFileExist:(NSString *)path
{
return [[NSFileManager defaultManager] fileExistsAtPath:path];
}

+(BOOL) isFolderExist:(NSString *)path
{
BOOL isDir;
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir];
return exists && isDir;
}

+(BOOL) copyFile:(NSString *)src dst:(NSString *)dst
{
NSFileManager *fmanager = [NSFileManager defaultManager];

//NSLog(@"Source file path: %@", src);
//NSLog([fmanager fileExistsAtPath:src] ? @"Source file exists": @"Source file doesn't exist");
//NSLog(@"Dest file path: %@", dst);

NSAssert([fmanager fileExistsAtPath:src], @"Source file does not exist: %@", src);

if ([fmanager fileExistsAtPath:dst] == YES)
{
[fmanager removeItemAtPath:dst error:nil];
}

return [fmanager copyItemAtPath:src toPath:dst error:nil];
}

+(BOOL) copyToDocuments:(NSString *)file
{
return [FileUtility copyFile:file dst:[[FileUtility getDocumentPath] stringByAppendingPathComponent:[file lastPathComponent]]];
}

+(BOOL) createFolder:(NSString *)folder{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSAssert(![fileManager fileExistsAtPath:folder], @"Cannot create folder as file/folder of the same name already exists: %@", folder);
return [fileManager createDirectoryAtPath:folder withIntermediateDirectories:YES attributes:nil error:nil];
}

#pragma end


@end
8 changes: 8 additions & 0 deletions src/ios/InAppPurchase.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@
#import <Cordova/NSData+Base64.h>

#import "SKProduct+LocalizedPrice.h"
#import "FileUtility.h"

@interface InAppPurchase : CDVPlugin <SKPaymentTransactionObserver> {
NSMutableDictionary *list;
NSMutableDictionary *retainer;
NSMutableDictionary *unfinishedTransactions;
NSMutableDictionary *currentDownloads;
}
@property (nonatomic,retain) NSMutableDictionary *list;
@property (nonatomic,retain) NSMutableDictionary *retainer;
@property (nonatomic, retain) NSMutableDictionary *currentDownloads;
//keep a reference to the transaction observer, to make sure we have only 1 call
@property (nonatomic,assign) id <SKPaymentTransactionObserver> observer;

Expand All @@ -32,9 +35,14 @@
- (void) appStoreReceipt: (CDVInvokedUrlCommand*)command;
- (void) appStoreRefreshReceipt: (CDVInvokedUrlCommand*)command;

- (void) pause: (CDVInvokedUrlCommand*)command;
- (void) resume: (CDVInvokedUrlCommand*)command;
- (void) cancel: (CDVInvokedUrlCommand*)command;

- (void) paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions;
- (void) paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error;
- (void) paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue;
- (void) paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads;

- (void) debug: (CDVInvokedUrlCommand*)command;
- (void) noAutoFinish: (CDVInvokedUrlCommand*)command;
Expand Down
Loading

0 comments on commit c9044ea

Please sign in to comment.