Tel. 06151 / 39 10 793

Implementierung eines InApp Stores auf dem iPhone 2 / 2

Bestätigung des InApp Kaufs in der Sandbox

Dies ist Teil zwei des Tutorials.
Im ersten Teil haben wir einen InApp Shop soweit gebaut, dass uns dort kaufbare Produkte Angezeigt werden.
In diesem Teil führen wir den Kauf durch.

Wir sorgen als erstes dafür,
dass wir wissen, welches Produkt unser Nutzer auswählt.
In der Klasse StoreManager implementieren wir,
eine Methode mit dem Namen canMakePurchases.
Diese Methode soll uns mitteilen,
ob eine Transaktion überhaupt möglich ist.

StoreManager.h

- (BOOL)canMakePurchases;

StoreManager.m

- (BOOL)canMakePurchases
{
return [SKPaymentQueue canMakePayments];
}

Wir wechseln wieder zur Klasse StoreViewController und
implementieren unsere Delegate Methode "tableView:didSelectRowAtIndexPath:",
die aufgerufen wird, sobald der Benutzer ein Produkt in unserem UITableView gewählt hat.

StoreViewController.m

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
SKProduct *product = [myStoreManager.validProducts objectAtIndex:indexPath.row];
NSLog(@"Product: %@",product.localizedTitle);

if ([myStoreManager canMakePurchases])
{
NSLog(@"PURCHASE: YES");
}
else
{
NSLog(@"PURCHASE: NO");
}
}

Als erstes lassen wir uns den Titel des gewählten Produktes in der Konsole ausgeben.
Die anschließende If-Bedingung meldet uns, ob ein Kaufvorgang überhaupt durchgeführt werden kann.
Betätigen Sie Build&Run um zu testen ob es funktioniert hat.

Starten wir von diesem Punkt nun den Verkaufsdialog.
Zunächst benötigen wir in StoreManager eine Methode,
die den InApp Purchase Prozess startet.

StoreManager.h

- (void)purchase:(SKProduct*)product;

StoreManager.m

- (void)purchase:(SKProduct*)product
{
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}

Die erste Zeile erzeugt einen Verkaufsvorgang.

In der zweiten Zeile wird dieser Vorgang der PaymentQueue hinzugefügt.
Das bedeutet, dass der Vorgang eingeleited wird.
Kaufvorgänge in der PaymentQueue bleiben dort auch wenn die App beendet wird. Sie sollten damit rechnen, dass ältere Vorgänge bei einem Neustart der App abgeschlossen werden könnten. Sind mehrere Vorgänge in der PaymentQueue, werden Sie nicht unbedingt in einer feststehenden Reihenfolge abgehandelt.

Der Verkaufsdialog finde mit dem App Store statt.
Alles was wir tun müssen, ist zuhören, wie das Ergebnis ausfällt.
Damit wir "zuhören" können, müssen wir einen Observer angeben.
Das machen wir in unserer init Methode mit folgender Zeile:
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];

Und so implementieren wir den Observer:

StoreManager.m

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchasing:
NSLog(@"Transaction: Purchasing");
                break;
case SKPaymentTransactionStatePurchased:
                NSLog(@"Transaction: Purchased");
                break;
            case SKPaymentTransactionStateFailed:
NSLog(@"Transaction: Failed");
                break;
            case SKPaymentTransactionStateRestored:
NSLog(@"Transaction: Restored");
                break;
            default:
NSLog(@"default");
                break;
        }
    }
}

Die SKPaymentQueue kann vier Zustände annehmen:
Purchasing: Der Kauf ist noch nicht abgeschlossen.
Purchased: Der Kauf wurde erfolgreich abgeschlossen.
Failed: Der Kauf wurde nicht erfolgreich abgeschlossen.
Restored: Der Benutzer hat dieses Produkt bereits gekauft und möchte es neu laden.

Um alles Testen zu können, erweitern wir unsere Delegate Methode im StoreViewController,
um eine Zeile:

StoreViewController.m

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   
NSLog(@"select row");

SKProduct *product = [myStoreManager.validProducts objectAtIndex:indexPath.row];
NSLog(@"Product: %@",product.localizedTitle);

if ([myStoreManager canMakePurchases])
{
NSLog(@"PURCHASE: YES");
[myStoreManager purchase:product];
}
else
{
NSLog(@"PURCHASE: NO");
}
}

Wenn Sie den Kaufvorgang nun testen möchten,
melden Sie sich am Endgerät vorher mit Ihrem iTunes Account ab.
Tragen Sie auch nicht den Testuser ein.
Verwenden Sie die Daten des Testusers erst in Ihrer App,
während des Verkaufdialogs.
Sollten Sie noch keinen Testuser angelegt haben,
können Sie das in iTunes Connect nachholen.

Wenn unsere Produkte bereits in der App vorhanden sind,
dann können Sie nun an dieser Stelle freigeschaltet werden und
der InApp Kauf ist abgeschlossen.
Wir gehen nun noch weiter und müssen unsere Produkte vom Server laden.
Dabei muss der Server selbst noch bestätigt bekommen,
dass das geforderte Produkt auch wirklich bezahlt wurde.

Wir Starten mit einer etwas größeren Methode in StoreManager,
die unseren Server darüber benachrichtigt, dass ein Kauf statt gefunden hat:

StoreManager.m

- (void)purchasedTransaction:(SKPaymentTransaction *)transaction
{
    // SERVER REQUEST
NSString *jsonObjectString = [self encode:(uint8_t *)transaction.transactionReceipt.bytes
   length:transaction.transactionReceipt.length];
NSString *productID = transaction.payment.productIdentifier;
NSString *post = [NSString stringWithFormat:@"receipt=%@&id=%@",jsonObjectString,productID];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSString *postLength = [NSString stringWithFormat:@"%d", [postData length]];
NSString *requestURLString = [NSString stringWithFormat:@"http://www.Ihre-URL-fuer-Produkte.de/Unterordner"];                              
NSURL *reqestURL = [NSURL URLWithString:requestURLString];              
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:reqestURL];                         
    [request setHTTPMethod:@"POST"];
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[request setHTTPBody:postData];
    NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil]; 
[request release];

// QUIT THE TRANSACTION
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];

NSString *testOutput = [[NSString alloc] initWithData:response encoding:NSASCIIStringEncoding];
NSLog(@"RESPONSE: %@",testOutput);
[testOutput release];
}

Was passiert hier?
Zuerst erzeugen wir einen encodierten String für JSON.
(Auf Encodierung werde ich in diesem Tutorial aber nicht weiter eingehen.)
Anschließend stellen wir fest, zu welcher Produkt ID unsere beendete Transaktion gehört.
Aus diesen beiden Werten bauen wir einen String, der später per HTTP-POST an unseren Server geschickt wird.
Wir wandeln den String in ein NSData Objekt um und stellen seine Länge fest.
Dann setzen wir unsere Ziel-URL und erzeugen damit ein Request Objekt.
Diesem Request weisen wir noch ein paar Werte zu, die wir zuvor ermittelt haben und
schicken es ab. Unsere Antwort kommt in "response" als NSData Objekt zurück.
Zuletzt beenden wir unsere Transaktion und geben die empfangenen Daten zum testen in der Konsole aus.

Hier noch die Methode für die Encodierung:

- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length
{
    static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    NSMutableData *data = [NSMutableData dataWithLength:((length + 2) / 3) * 4];
    uint8_t *output = (uint8_t *)data.mutableBytes;

    for (NSInteger i = 0; i < length; i += 3) {
        NSInteger value = 0;
        for (NSInteger j = i; j < (i + 3); j++) {
value <<= 8;

if (j < length) {
value |= (0xFF & input[j]);
}
        }

        NSInteger index = (i / 3) * 4;
        output[index + 0] =                    table[(value >> 18) & 0x3F];
        output[index + 1] =                    table[(value >> 12) & 0x3F];
        output[index + 2] = (i + 1) < length ? table[(value >> 6)  & 0x3F] : '=';
        output[index + 3] = (i + 2) < length ? table[(value >> 0)  & 0x3F] : '=';
    }
    return [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease];
}

Nun ist wichtig was auf dem Server passiert.
Das muss natürlich implementiert sein.

Der Server erhält von uns per Post einen Beleg der Transaktion und die ID des gekauften Produktes.
Mit diesem "Beleg" muss unser Server nun bei iTunes anfragen,
ob die Transaktion, die zu diesem Beleg gehört, auch wirklich durchgeführt wurde.
Wir erzeugen auf dem Server mit unseren Daten ein JSON Objekt mit dem Namen receipt-data.

{
"reseipet-data" : "(der Beleg der Transaktion)"
}

Dann senden wir dieses Objekt von unserem Server mit einem HTTP POST Request an iTunes.

URL zum Testen:
https://sandbox.itunes.apple.com/verifyReceipt

URL für den späteren endgültigen Shop:
https://buy.itunes.apple.com/verifyReceipt

Als Antwort erhalten wir ein JSON Objekt mit zwei Keys.

{
    "status" : 0,
    "receipt" : { ... }
}

Wenn der Status der Antwort 0 ist, dann wurde uns der Kauf des Produktes von iTunes bestätigt.
Das bedeutet, wir liefern unserer App das geforderte Produkt in Form von XML als Antwort.
Diese Antwort landet dann in unserem NSData Objekt "response".
Nun können Sie Ihre XML Daten in der App Speichern und damit machen, wofür Sie gedacht sind.

Es fehlt natürlich noch, dass wir unsere purchasedTransaction: -Methode ausführen.
Das machen wir indem wir den Methodenaufruf in unserem Observer ergänzen:

StoreManager.h

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchasing:
NSLog(@"Transaction: Purchasing");
                break;
case SKPaymentTransactionStatePurchased:
                NSLog(@"Transaction: Purchased");
[self purchasedTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
NSLog(@"Transaction: Failed");
                break;
            case SKPaymentTransactionStateRestored:
NSLog(@"Transaction: Restored");
[self purchasedTransaction:transaction];
                break;
            default:
NSLog(@"default");
                break;
        }
    }
}

In diesem Fall, wird unsere Methode purchasedTransaction: aufgerufen wenn der Vorgang erfolgreich war oder
wenn der Benutzer den Artikel bereits gekauft hat und erneut laden möchte.
Das Resultat ist das gleiche, außer dass der User das Produkt im zweitenb Fall nicht erneut bezahlen muss.

Wir sind am Ende des Tutorial angekommen. Ich hoffe es war hilfreich.
Denken Sie daran auch zu implementieren, was passieren soll, wenn Transaktionen scheitern und wie sich der View verändert, nachdem ein Produkt gekauft wurde.
Weitere Informationen zum Thema InApp Purchase finden Sie in den offiziellen Dokumentationen von Apple.

Zum Schluss noch, wie der gesamte Quelltext der Klassen nun aussehen sollte:

StoreViewController.h

#import <UIKit/UIKit.h>
#import <StoreKit/StoreKit.h>
#import "StoreManager.h"

@interface StoreViewController : UITableViewController
{
NSArray *shopArray;
StoreManager *myStoreManager;
}

@property (nonatomic, retain) NSArray *shopArray;
@property (nonatomic, retain) StoreManager *myStoreManager;

- (void) loadShopData;
- (void) displayShop: (NSArray *) theShopArray;

@end

StoreViewController.m

#import "StoreViewController.h"


@implementation StoreViewController

@synthesize shopArray, myStoreManager;


#pragma mark -
#pragma mark Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    NSLog(@"SECTION");
return 1;
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSLog(@"ROW: %i", [shopArray count]);
return [shopArray count];
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   
    static NSString *CellIdentifier = @"Cell";
   
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }
   
SKProduct *thisProduct = [shopArray objectAtIndex:indexPath.row];
    cell.textLabel.text = thisProduct.localizedTitle;
   
    return cell;
}


#pragma mark -
#pragma mark own methods

- (void) loadShopData
{
StoreManager *storeManagerForLoadingContent = [[StoreManager alloc] initWithStoreViewController:self];
self.myStoreManager = storeManagerForLoadingContent;
[storeManagerForLoadingContent release], storeManagerForLoadingContent = nil;
}

- (void) displayShop: (NSArray *) theShopArray
{
self.shopArray = [NSArray arrayWithArray:theShopArray];
[self.view reloadData];
}


#pragma mark -
#pragma mark Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   
NSLog(@"select row");

SKProduct *product = [myStoreManager.validProducts objectAtIndex:indexPath.row];
NSLog(@"Product: %@",product.localizedTitle);

if ([myStoreManager canMakePurchases])
{
NSLog(@"PURCHASE: YES");
[myStoreManager purchase:product];
}
else
{
NSLog(@"PURCHASE: NO");
}
}



#pragma mark -
#pragma mark Memory management

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];

}

- (void)dealloc {
[shopArray release], shopArray = nil;
[myStoreManager release], myStoreManager = nil;
    [super dealloc];
}

@end

StoreManager.h

#import <Foundation/Foundation.h>
#import <StoreKit/StoreKit.h>
@class StoreViewController;


@interface StoreManager : NSObject <SKProductsRequestDelegate> {

NSMutableArray *productIDs; 
NSMutableArray *validProducts;

StoreViewController *myStoreViewController;
}

@property (nonatomic, retain) NSMutableArray *productIDs;
@property (nonatomic, retain) NSMutableArray *validProducts;

- (id) initWithStoreViewController: (StoreViewController *) theStoreViewCotroller;
- (BOOL)canMakePurchases;
- (void)purchase:(SKProduct*)product;

@end

StoreManager.m

#import "StoreManager.h"


@implementation StoreManager

@synthesize productIDs, validProducts;



#pragma mark -
#pragma mark XML parser delegate

- (void) parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict
{
NSLog(@" JODEL ");
NSString *myProductID = [[NSString alloc] init];
if ([elementName isEqualToString:@"product"]) {
NSString *myProductID = [@"com.beispielfirma.appname." stringByAppendingString:[attributeDict objectForKey:@"nid"]];
[productIDs addObject:myProductID];
}
[myProductID release];
}



#pragma mark -
#pragma mark product requesting delegate

-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
{
[validProducts addObjectsFromArray: response.products];
[request release];
}

-(void)requestDidFinish:(SKRequest *)request
{
    [myStoreViewController displayShop:validProducts];
[myStoreViewController release];
}



#pragma mark -
#pragma mark own methods

- (void) loadStoreData
{
NSURL *shopURL = [NSURL URLWithString:@"http://www.beispiel.url/beispiel.xml"];
NSData *shopContent = [NSData dataWithContentsOfURL:shopURL];

NSXMLParser *parser = [[NSXMLParser alloc] initWithData:shopContent];
[parser setDelegate:self];
[parser parse];
[parser release];

SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIDs]]; 
    request.delegate = self; 
    [request start];
}

- (BOOL)canMakePurchases
{
return [SKPaymentQueue canMakePayments];
}

- (void)purchase:(SKProduct*)product
{
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
SKPayment *payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}

- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length
{
    static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    NSMutableData *data = [NSMutableData dataWithLength:((length + 2) / 3) * 4];
    uint8_t *output = (uint8_t *)data.mutableBytes;

    for (NSInteger i = 0; i < length; i += 3) {
        NSInteger value = 0;
        for (NSInteger j = i; j < (i + 3); j++) {
value <<= 8;

if (j < length) {
value |= (0xFF & input[j]);
}
        }

        NSInteger index = (i / 3) * 4;
        output[index + 0] =                    table[(value >> 18) & 0x3F];
        output[index + 1] =                    table[(value >> 12) & 0x3F];
        output[index + 2] = (i + 1) < length ? table[(value >> 6)  & 0x3F] : '=';
        output[index + 3] = (i + 2) < length ? table[(value >> 0)  & 0x3F] : '=';
    }
    return [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease];
}

- (void)purchasedTransaction:(SKPaymentTransaction *)transaction
{
    // SERVER REQUEST
NSString *jsonObjectString = [self encode:(uint8_t *)transaction.transactionReceipt.bytes
   length:transaction.transactionReceipt.length];
NSString *productID = transaction.payment.productIdentifier;
NSString *post = [NSString stringWithFormat:@"receipt=%@&id=%@",jsonObjectString,productID];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSString *postLength = [NSString stringWithFormat:@"%d", [postData length]];
NSString *requestURLString = [NSString stringWithFormat:@"hhttp://www.Ihre-URL-fuer-Produkte.de/Unterordner"];                              
NSURL *reqestURL = [NSURL URLWithString:requestURLString];              
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:reqestURL];                         
    [request setHTTPMethod:@"POST"];
[request setValue:postLength forHTTPHeaderField:@"Content-Length"];
[request setHTTPBody:postData];
    NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil]; 
[request release];

// BEENDE DIE TRANSAKTION
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];

NSString *testOutput = [[NSString alloc] initWithData:response encoding:NSASCIIStringEncoding];
NSLog(@"RESPONSE: %@",testOutput);
[testOutput release];
}



#pragma mark -
#pragma mark observer

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
    for (SKPaymentTransaction *transaction in transactions)
    {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchasing:
NSLog(@"Transaction: Purchasing");
                break;
case SKPaymentTransactionStatePurchased:
                NSLog(@"Transaction: Purchased");
[self purchasedTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed:
NSLog(@"Transaction: Failed");
                break;
            case SKPaymentTransactionStateRestored:
NSLog(@"Transaction: Restored");
[self purchasedTransaction:transaction];
                break;
            default:
NSLog(@"default");
                break;
        }
    }
}



#pragma mark -
#pragma mark init & memory management

- (id) init
{
StoreManager *sm = [super init];
if (sm) {
sm.productIDs = [NSMutableArray array];
sm.validProducts = [NSMutableArray array];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
return sm;
}
return nil;
}

- (id) initWithStoreViewController:(StoreViewController *) theStoreViewCotroller
{
StoreManager *sm = [self init];
if (sm)
{
myStoreViewController = [theStoreViewCotroller retain];
[self loadStoreData];
return sm;
}
return nil;
}

- (void)dealloc {
[productIDs release], productIDs = nil;
[validProducts release], validProducts = nil;
    [super dealloc];
}

@end

Bereits gekaufte Artikel
Erfolgreiche Transaktion

Kommentar hinzufügen

Der Inhalt dieses Feldes wird nicht öffentlich zugänglich angezeigt.
CAPTCHA
Diese Frage hat den Zweck zu testen, ob Sie ein menschlicher Benutzer sind und um automatisierten Spam vorzubeugen.
So finden Sie uns