Dejal

This site is designed to take advantage of CSS. If you are seeing this, CSS must not be available or enabled in your browser. Everything should still work, but won't be as pretty. :)

development

A professional press release via prMac

On Monday I did a major upgrade of Dejal Narrator, my app to read out stories in multiple voices, to version 2.0. I usually send out press releases when I do major and minor product releases, but have previously just written and sent the releases myself, using a collection of email addresses I've gathered over the years.

But for this release, I decided to try something different. I had tried free distributions via prMac.com in the past, often while doing my own releases too. It seemed like a good service, but the three-day delay for free releases lacked the immediacy I wanted. So this time I put it to a real test: I used their Writing Service to craft a press release using their experience and skills to get the message across, and paid for the Extended Distribution to get the release out immediately and to a wider audience.

So how'd it work for me? Ray from prMac was prompt and friendly, quickly crafting a release that captured the essence of the product. There was opportunity to review and tweak the wording, but very few changes were needed. Then on release day, I submitted it for posting, which was done soon afterwards. I quickly noticed lots of sites mentioning Narrator, that normally don't pick up my press releases (such as Macworld). The prMac service definitely has a much wider range of publishers than my self-created list.

I'm not sure if it's related or not, but I was very pleasantly surprised to discover that the Apple Downloads software listing site selected Narrator as a "Staff Pick"... not only showing it as the "Featured Download" on the Home & Learning category page, but as the "Featured Download" at the top of the All Categories page, for a couple of days (it's been bumped now, though). Quite the honor! I'm willing to give prMac the credit for gaining Apple's attention like that. You can't buy publicity like such a prominent spot on Apple's site, but for a few dollars you can buy an excellent press release distribution. I plan on using prMac again for future releases.

Narrator as Apple's Featured Download

Migrating from disk images to ZIP archives

For many years, I've been releasing my Mac OS X software on disk images. For a long time, they seemed the most elegant way to provide software to people: they provide a single downloadable container that can be saved in a compressed state, and they can include a pretty background image that explains how to install the app.

A more recent innovation was to include an alias (actually a symbolic link) of the Applications folder, with an arrow in the background indicating to drag the application to that folder to install:

Simon disk image

But all that complicates the release process for me, and for my customers.

For me, when I do a release build I need to copy the build to a standard location then run FileStorm, an application that builds the disk image, then upload the resulting disk image. Doesn't sound too hard, except that FileStorm tended to misbehave for me all too often, resulting in incorrectly laid-out disk images and other problems, requiring several attempts to get it right. It also had compatibility issues with Leopard, forcing me to run it on a Tiger machine, which had other complications.

For my customers, disk images have more hassles. After downloading, they are usually mounted automatically by the OS, though sometimes that didn't work for some people. The images are "internet enabled", so people downloading via Safari get only the contents of the disk images, while people using other browsers get the disk image window as above. Then they need to find it and drag the application to install it... but some people run it directly off the disk image, then wonder where it went after they've dismounted the disk image (or restarted their computer). Plus the disk image has to be dismounted, another hassle.

There's got to be a better way... and there is. The humble ZIP archive.

A ZIP archive is a simple compressed file. They can be created and expanded using built-in commands in the Finder. So for me, creating one is a trivial operation; no more messing around with FileStorm. And they are more convenient for my customers too. After downloading, the archive is automatically expanded, with the application appearing in the download folder. They can then easily install it by dragging to the Applications folder, or try it directly from the downloads folder if preferred — without it mysteriously vanishing after a restart.

So, as I release new versions of my apps, I have been switching to the ZIP archive format. Downloads work exactly the same from my site, but the result is much more convenient for everyone.

As always, I welcome feedback on this. So far, I haven't had any complaints, though one potential problem has come up: one person with an incorrectly installed copy of StuffIt had difficulty expanding the archive by double-clicking on it. The solution was simply to tell the Finder to use the built-in archive expander instead (which is called "BOMArchiveHelper" on Tiger or "Archive Utility" on Leopard).

Cocoa: custom attachment in a text view

I recently posted a query to CocoaDev asking for help with inserting a custom attachment cell into a text view. I had spent quite some time investigating and experimenting, and searches showed several people who had the same question, but no satisfactory answers. Some comments I read suggested subclassing NSTextView, which I tried, but not very satisfactorily.

Fortunately, Douglas Davidson was kind enough to point me in the right direction, combined with helpful off-list discussion with another developer who was working on much the same problem.

I thought I'd share my solution here, in case it helps anyone else. It turned out to be easier than I'd expected. Note: this is written for Leopard with Garbage Collection, so doesn't have releases etc.

Firstly, a file wrapper is used to create a placeholder TIFF with a special "sub-extension" to identify it. I could store any kind of data, but I use a placeholder image so if the user pastes the text into another app, it shows up as something nicer than a generic document icon. The identifier is a unique reference to the data that the attachment represents:

- (NSFileWrapper *)fileWrapperWithIdentifier:(NSString *)identifier;
{
    NSString *wrapName = [[identifier stringByAppendingPathExtension:@"myspecialmarker"] stringByAppendingPathExtension:@"tiff"];
    NSSize size = {100, 18};
    NSData *data = [[NSImage imageNamed:@"AttachmentPlaceholder"] TIFFRepresentation];
    NSFileWrapper *wrapper = [[NSFileWrapper alloc] initRegularFileWithContents:data];
   
    [wrapper setFilename:wrapName];
    [wrapper setPreferredFilename:wrapName];
   
    return wrapper;
}

This method inserts the attachment with the above wrapper and my custom cell:

- (void)insertMarkerWithIdentifier:(NSString *)identifier;
{
   NSFileWrapper *wrapper = [self fileWrapperWithIdentifier:identifier];
   NSTextAttachment *attachment = [[NSTextAttachment alloc] initWithFileWrapper:wrapper];
   MyAttachmentCell *cell = [MyAttachmentCell new];

   cell.identifier = identifier;
   [attachment setAttachmentCell:cell];

   [[myTextView textStorage] appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
}

I use a couple of text view delegate methods to write the cell's file wrapper to the pasteboard as file contents, allowing it to be copied, dragged, and saved to disk with the text:

- (NSArray *)textView:(NSTextView *)aTextView writablePasteboardTypesForCell:(id <NSTextAttachmentCell>)cell
             atIndex:(NSUInteger)charIndex;
{
   return [NSArray arrayWithObject:NSFileContentsPboardType];
}

- (BOOL)textView:(NSTextView *)aTextView
       writeCell:(id <NSTextAttachmentCell>)cell
         atIndex:(NSUInteger)charIndex
    toPasteboard:(NSPasteboard *)pboard type:(NSString *)type;
{
   if (type == NSFileContentsPboardType)
       [pboard writeFileWrapper:[[cell attachment] fileWrapper]];

   return YES;
}

And to convert the attachment back to my custom cell after pasting/dragging/loading it, the text storage delegate (which is set via [[myTextView textStorage] setDelegate:self];). It looks for TIFF attachments that have my special marker "sub-extension" but aren't using my custom cell, and replaces their cell with my custom one:

- (void)textStorageWillProcessEditing:(NSNotification *)note;
{
   NSAttributedString *text = [myTextView textStorage];

   if ([note object] != text)
       return;

   NSUInteger length = [text length];
   NSRange effectiveRange = NSMakeRange(0, 0);
   id attachment;

   while (NSMaxRange(effectiveRange) < length)
   {
       attachment = [text attribute:NSAttachmentAttributeName atIndex:NSMaxRange(effectiveRange) effectiveRange:&effectiveRange];

       if (attachment)
       {
           if ([attachment isKindOfClass:[NSTextAttachment class]] &&
               ![[attachment attachmentCell] isKindOfClass:[MyAttachmentCell class]] &&
               [[[[attachment fileWrapper] preferredFilename] pathExtension] isEqualToString:@"tiff"] &&
               [[[[[attachment fileWrapper] preferredFilename] stringByDeletingPathExtension] pathExtension] isEqualToString:@"myspecialmarker"])
           {
               MyAttachmentCell *cell = [MyAttachmentCell new];
               [cell setIdentifier:[[[[attachment fileWrapper] preferredFilename] stringByDeletingPathExtension] stringByDeletingPathExtension]];
               [attachment setAttachmentCell:cell];
           }
       }
   }
}

The custom attachment cell is a subclass of NSTextAttachmentCell, to simply draw in a custom way.

I hope this is helpful to others.

I'm the MacTech Spotlight for October 2007

MacTech magazine has a monthly feature at the back of each recent issue called "MacTech Spotlight", where they devote a page to Q&As with a developer or other personality in the Mac community. They've talked with Paul Kafasis of Rogue Amoeba, Wolf Rentszch of Red Shed, and several others.

This month was my turn.

I haven't seen how the article turned out yet (my copy's in the mail), but hopefully it'll be okay. :) I talked about how I originally got into computers and programming, how Dejal got started, what I like about Apple and Mac OS X, how I come up with product ideas, and more.

For those interested, MacTech offers discounts on subscriptions - 60% off the cover price, or a limited-time offer of a six-month sub for only $9.95.

Choosing big cats

LeopardWhenever a developer does a major new release, one of the many issues that they need to consider is which OS versions to support. It's always a tradeoff: having the apps continue to work with older versions allows more people to use the product, whereas the more it is restricted, the more new OS features can be leveraged.

Let's explore the second point first. Each time Apple does an OS upgrade, they make some cosmetic UI changes, add new bundled apps, and so forth, but they also introduce new or improved development frameworks, APIs, and tools. For example, in Tiger (10.4) Apple introduced Core Data, a sophisticated database-driven data modeling and storage API, among others. While it is possible to use some new functionality by checking for its availability, some are more fundamental, so just have to be done without to maintain support for older OS versions.

Apple's Non-Disclosure Agreement forbids me to talk about the new features in their upcoming OS, Leopard (10.5), other than what has been publicly announced. But it should be clear from the available information that Leopard is one of their most significant OS updates, with numerous UI changes, and for developers, Objective-C 2.0, which includes fundamentally different memory management and other improvements.

Being able to leverage the new technologies in Leopard will offer a huge advantage in terms of the features that can be added, and the efficiency of the underlying code. To maintain compatibility with Tiger, a developer would either have to forgo these features, or write lots of code to emulate them. Certainly doable... but is it worth it?

That brings us to the other side of the equation: how many people would benefit from a release that supports an older OS? Thankfully, history provides some guidance. Of course, it should be pointed out that every market is different, and people find software in lots of ways. Lots of people, probably the majority, just pick up a few boxed software products when they buy their Mac at a local Apple store or other retailer, and never explore beyond that. They only need a few core apps, many of which come bundled with the Mac, and that's all they want.

While I'd love to bring my products to the attention of these people, the ones that most interest me are those who actively seek out third-party indie apps to enhance their Macs. This subset tend to be early adopters, often upgrading their computers every few years, and thus getting the new OS then, or they go out and buy the OS as soon as it is released, or perhaps after an update or two, to ensure it is stable. In fact, one could hypothesize that there is a strong correlation between the speed of upgrading the OS and that of upgrading third-party apps. Or put another way, if someone isn't interested in keeping the OS up-to-date, they are probably less interested in keeping third-party apps up-to-date, or even trying third-party apps.

I have tracked OS versions of people using my products, as many other developers do, and these numbers support this hypothesis. For fascinating interactive charts showing the OS versions and other attributes of the Omni Group's customers, see their update charts.

So, there seems to be little benefit in maintaining support for older OS versions, and several benefits from being able to leverage the new technologies.

As for my own products, I do plan to require Leopard when I next do major upgrades of each product, but they will be phased in over time. I will continue doing updates that support 10.3.9 and later in the meantime. Even after the major upgrades, the older releases will continue to be available for customers who aren't ready to upgrade their OS. Hopefully that will keep most people happy, while allowing me to move forward.

Be your own customer

A comment by Chris Smolinski of Black Cat Systems on the Mac Small/Software Business mailing list (there doesn't seem to be any consensus if it's "Small" or "Software") inspired me to share my views on this: "Be your own customer."

This was in response to a question on how to get good ideas for software, with the popular opinion being that scratching your own itch can lead to great product opportunities. Find something that irritates you about existing products, or something you want to do that isn't covered by an app already, and create a solution.

I can vouch for this approach. Although my products started out in various ways, most of them started because there weren't any satisfactory solutions at the time. For example, Simon began as a way for me to watch for website updates, and became more sophisticated as it became popular. There weren't any easy-to-use Mac products for site monitoring at the time - and the ones that did exist don't anymore. Similarly, I created Time Out to improve my health, since I can suffer from eyestrain when staring at a computer for hours on end. BlogAssist was written specifically for my wife, who was really into LiveJournal blogging at the time... but I find I use it a lot, too. All of the products have grown and evolved over the years based on customer feedback, but also my own ideas, since I use them myself on a daily basis (e.g. I used BlogAssist's Services window four times to create the links in this post).

When you are your own customer, or "eat your own dog food", to use that colorful and somewhat unpleasant phrase, you notice little irritations in your initial implementation, and can do something about it. Chances are, your paying and potential customers are also noticing such issues. They can can also be a great source of ideas for better ways to do things that you may not have thought of, perhaps because you're so used to a certain way of doing things.

So if you're a developer looking for the next big hit, or just something to get you started, look inside yourself for your inspiration. Sure, an idea has to have some market appeal to be a success, but even seemingly niche products can be successful if you do a good job. Put your heart into it, and people will respect that. Keep working on it, and using it yourself, and word will spread.

I'm sick of users

There have been a few interesting posts in the blogosphere discussing the word "users", notably one by Josh Bernoff. He argues against using this word, suggesting that the word "users" should be replaced with "customers", "owners", "members", etc:

Users put up with computers. People just do stuff.

I completely agree. I have long disliked the word "users", and try to avoid it. I don't think of the people using my software as "users", but rather as "customers". I write software designed to solve problems, help people achieve tasks, maintain health, monitor sites, and more. They are written by people, for people, for real-world situations.

As I say in my About page, my customers are my friends. I really believe that; it's not just a catchy phrase, but a basic philosophy. When someone writes to me with a question, a problem, or a suggestion, I read it and reply personally, trying to put myself in their shoes and figure out how best to help them. I want them to be happy, not as a user of a product, but as a customer of my company, an existing or potential friend of myself. But my thoughts on customer service is a subject for a future blog post.

Unfortunately, a long history in the computer industry inflicts itself on me when I'm not looking, so the Dejal site does use the word "user" in a few places. For example, I have "User Guides" for my software, and I may use the term in other places. I'm not sure what would be a better term for that, though... "Customer Guide" doesn't sound right. Maybe I should just call it "Simon Guide", "Time Out Guide", etc?

So developers, please consider what words best describe your relationship with your customers. Are they anonymous automatons struggling to use your software, or are they genuine people who find your products useful to help them in their daily activities?

Future and current customers: please remember that I'm a person too, not an faceless corporate entity. I invite you to tell me what you like and dislike about my work, how I can change things to help you better, or simply what you do with my products. I care, I really do! Let's be friends!

Prompt PayPal payment processing, part 3

Here's the remainder of the PHP code for the recent automation of my PayPal-based store, using PayPal's Instant Payment Notification (IPN) service. See part 1 for the introduction and part 2 for the start of the code.

So now we've got a valid transaction, so can begin to process it. As I previously said, I use a shopping cart, so I need to loop over the cart contents. I call the getPayPalShoppingCartValue() function (provided in part 2) to get values specific to a product; shared values can be copied easily. This should all work without a shopping cart, too. I save the values into a $license associative array, which is equivalent to a Cocoa NSDictionary.

    $count = $transaction['num_cart_items'];
   
    if ($count < 1)
        $count = 1;
   
    // Process each shopping cart item as a separate license:
    for ($index = 0; $index < $count; $index++)
    {
        // Construct the license:
        $license = array();
       
        $license['Parser'] = 'PayPal';
        $license['Parameters'] = $transaction;
        $license['Name'] = $transaction['first_name'] . " " . $transaction['last_name'];
        $license['Email'] = $transaction['payer_email'];
        $license['Postal'] = $transaction['address_street'] . ", " . $transaction['address_city'] . ", ";
        $license['Postal'] .= $transaction['address_state'] . " " . $transaction['address_zip'] . ", " . $transaction['address_country'];
        $license['PaymentAmount'] = getPayPalShoppingCartValue($transaction, 'mc_gross', $index);
        $license['TotalAmount'] = $transaction['mc_gross'];
        $license['Quantity'] = getPayPalShoppingCartValue($transaction, 'quantity', $index);
        $license['ProcessorNote'] = $transaction['payment_date'];
        $license['ProcessorTransID'] = $transaction['txn_id'];
        $license['ProcessorName'] = 'PayPal';
        $license['PaymentMethod'] = 'PayPal';
       
        $ref = getPayPalShoppingCartValue($transaction, 'option_selection1', $index);
       
        if (!$ref)
            $ref = 'paypal';
       
        $license['Referrer'] = $ref;
        $license['Special'] = getPayPalShoppingCartValue($transaction, 'option_selection2', $index);
       
        if ($testMode)
            $license['Test'] = 1;
       
        $license['ServerReferrer'] = $_SERVER['HTTP_REFERER'];
        $license['ServerIP'] = $_SERVER['REMOTE_ADDR'];
        $license['ServerAgent'] = $_SERVER['HTTP_USER_AGENT'];
       
        $itemName = getPayPalShoppingCartValue($transaction, 'item_name', $index);
        $isDonation = strcontains($itemName, 'Donation', true);
       
        setLicenseProduct($license, $itemName);

The setLicenseProduct() function simply maps the shopping cart's item name to my own product identifier.

Time to actually add the license. This is done with another function to be left as an exercise for the reader, addLicense(). It takes the $license array we just constructed, adds it to the license database, and returns (by reference) any error. If the sale was a donation for a freeware product or a pending sale, though, the add is bypassed, since a donation doesn't need a license, and a pending sale (if it reaches here) shouldn't be issued one.

We then construct an email to the customer, with different wording for freeware, pending, or normal sales. I'm omitting most of that, since it's specific to your situation:

        // Add the license, unless it's a donation or pending:
        if ($isDonation || $isPending || addLicense($license, $error))
        {
            $productName = $license['ProductName'];
            $version = $license['Version'];
            $name = $license['Name'];
            $email = $license['Email'];
           
            // Donations and pending transactions will use the item name for the product name,
            // as the latter is set in updateLicense(), which isn't called for them:
            if (!$productName)
                $productName = $itemName;
           
            if ($isDonation)
                $body = "Greetings.  Thank you for the $productName!  I appreciate it.\n\n\n";
            else if ($isPending)
            {
                $body = "Greetings.  Thank you for purchasing a $productName.\n\n\n";
                $body .= "Your PayPal transaction is still pending, so you will receive your license when the payment is complete.\n\n\n";
            }
            else
            {
                $body = "Greetings.  Thank you for purchasing $productName.\n\n\n";
                $body .= "Licensed Name: $name\n";
                $body .= "Licensed Email: $email\n";
                $body .= "License Kind: " . $license['KindName'] . "\n";
                $body .= "$productName $version Serial Number: " . $license['Serial'] . "\n\n\n";
                // Instructions on adding the license omitted...
            }
           
            // Info on forums, FAQ, email contacts, etc omitted...
           
            $headers = "From: MyCompany <sales@mycompany.com>\r\n";
            $headers .= "Bcc: MyCompany <sales@mycompany.com>\r\n";
            $headers .= "X-Mailer: PHP/" . phpversion() . "\r\n";
           
            if ($isDonation)
                $subject = $productName;
            else if ($isPending)
                $subject = "$productName purchase";
            else
                $subject = "$productName license";
           
            mail("$name <$email>", $subject, $body, $headers);
        }
        else if ($error)
            exitWithError('PayPal', $error, $license);
    }
   
    header("Content-Type: text/text");
    echo("Transaction completed.");
}

And we're done! I hope this is helpful. This all seems to work fine for me, but if you spot any bugs or have any suggestions for improvements, please let me know. If you have any questions, I'd be happy to elaborate more. I'd be keen to hear from people who use this code, too.

Prompt PayPal payment processing, part 2

As discussed in part 1, I recently automated my PayPal-based store, using PayPal's Instant Payment Notification (IPN) service.

For those interested in the technical details, perhaps implementing this for your own store, here's my code. This is written in PHP, but other languages can be used too.

Firstly, a couple of utility functions. For getPayPalShoppingCartValue(), given the PayPal transaction data, a key, and the (zero-based) index, this returns the corresponding value. Tries the key with the index appended, with an underscore and the index appended, or by itself; the documentation is somewhat inconsistent on how it is applied, though I think an underscore is usually used. All this code should still work fine if you aren't using the shopping cart, too.

function getPayPalShoppingCartValue(&$transaction, $key, $index = 0)
{
    $value = $transaction[$key . ($index + 1)];
   
    if (!$value)
        $value = $transaction[$key . '_' . ($index + 1)];
   
    if (!$value)
        $value = $transaction[$key];
   
    return $value;
}

Next, the exitWithError() function emails me the details of an error, for diagnostic purposes, then exits the script. You can optionally pass an array and it will be included in the email. It also outputs the error (normally wouldn't be seen). It calls another existing function of mine (not provided here) that returns the array as an ASCII property list; there are other ways to output it too. Of course, you should replace the mycompany.com email addresses with your own.

function exitWithError($parser, $error = '', $array = null)
{
    $body = "An error occurred with the $parser parser:\n\n$error\n\n";
   
    if ($array)
        $body .= arrayToASCIIPropertyList($array);
   
    $headers = "From: MyCompany <info@mycompany.com>\r\n";
    $headers .= "X-Mailer: PHP/" . phpversion() . "\r\n";
   
    mail("info@mycompany.com", "Store error: $error", $body, $headers);
   
    header("Content-Type: text/text");
    echo("$parser error: $error");
   
    exit();
}

On to the main code. I actually have this in a function in my code, as it is just one processor function among others, but if you only have one, it can be at the top level of the script.

function handlePayPal()
{
    // Copy to a local variable for convenience, and since the post back to PayPal might wipe it:
    $transaction = $_POST;
   
    // Test mode is activated by passing test=1 to my PayPal Store.  It then uses PayPal's sandbox site instead:
    $testMode = $transaction['test_ipn'] == 1;
   
    if ($testMode)
    {
        mail("MyCompany <sales@mycompany.com>", "PayPal IPN starting", arrayToASCIIPropertyList($transaction), "From: MyCompany <sales@mycompany.com>\r\n");
       
        $paypalDomain = 'www.sandbox.paypal.com';
        $receiverEmail = 'paypal_sandbox_biz@mycompany.com';
    }
    else
    {
        $paypalDomain = 'www.paypal.com';
        $receiverEmail = 'paypal@mycompany.com';
    }

The above sets up things based on whether the purchase was via the PayPal sandbox or your live store. The sandbox is a great way to create fake customer and seller accounts for testing without spending real money. You can initiate this test mode by changing the action on your form from <https://www.paypal.com/cgi-bin/webscr> to <https://www.sandbox.paypal.com/cgi-bin/webscr>. You also need to change the form's "business" field to the sandbox receiver email address.

Next up, we post the received data back to PayPal, so they can confirm that they actually sent it. We construct and post urlencoded form data to their server, then fetch the response. If it's VERIFIED, we're good:

    // Read the post from PayPal system and add 'cmd':
    $req = 'cmd=_notify-validate';
   
    foreach ($_POST as $key => $value)
    {
        $value = urlencode(stripslashes($value));
        $req .= "&$key=$value";
    }
   
    // Post back to PayPal system to validate:
    $header .= "POST /cgi-bin/webscr HTTP/1.0\r\n";
    $header .= "Host: $paypalDomain:80\r\n";
    $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
    $header .= "Content-Length: " . strlen($req) . "\r\n\r\n";
   
    $fp = fsockopen($paypalDomain, 80, $errno, $errstr, 30);
   
    if (!$fp)
        exitWithError('PayPal', 'Unable to connect to the PayPal server.', $transaction);
   
    fputs($fp, $header . $req);
   
    $verified = false;
   
    // Read the response:
    while (!feof($fp))
    {
        $line = fgets($fp, 1024);
       
        if (strcmp($line, "VERIFIED") == 0)
            $verified = true;
    }
   
    fclose($fp);
   
    // Ensure PayPal verified the post:
    if (!$verified)
        exitWithError('PayPal', 'Received a transaction that PayPal did not verify as valid.', $transaction);

We then do some further checks, to ensure the payment went to the correct address, and the transaction has an acceptable status. I wrote this before realizing that pending transactions probably don't reach this point anyway (I think PayPal holds off notifying till they are completed), but it doesn't hurt to leave the pending logic in here:

    // Ensure the payment went to me:
    if ($transaction['receiver_email'] != $receiverEmail)
        exitWithError('PayPal', 'The receiver email was not correct.', $transaction);
   
    $transStatus = $transaction['payment_status'];
    $isPending = ($transStatus == 'Pending');
   
    if (!$isPending && $transStatus != 'Completed')
        exitWithError('PayPal', "The transaction status was $transStatus.", $transaction);

So now we've got a valid transaction, so can begin to process it. Continue to part 3 with the remainder of the code!

Prompt PayPal payment processing, part 1

I use two payment processors for Dejal: Kagi and PayPal. The Dejal Store powered by Kagi has been fully automated for years, so when a customer buys one of my products, Kagi queries my server, which generates a serial number and passes it back to Kagi's server, and the customer receives it automatically as part of Kagi's "Thanks for your purchase" message. This is very convenient for both me and the customer. (My products also include a Kagi-based store right within the app, which avoids the need to even enter the serial number.)

For the Dejal Store powered by PayPal, on the other hand, it has been a manual process. When someone purchased via PayPal, I received a notification of the payment in my email, which I copied to my home-grown license management app, which parsed it to generate the license, including creating an email message in Mail ready to send. This wasn't much work, but required manual processing on my part, which of course meant the customer had to wait for me before they got their license. I was usually very prompt, but if they bought while I was asleep or otherwise away from my machine, they had to wait. That's just not great service.

So this week I've spent the time to automate the PayPal processing. Now, if you buy from my PayPal store, PayPal will send my server a notification with the shopping cart contents. My server will then automatically add licenses and generate and send out email messages for each product purchased. So now purchasing from either store (or within the app) will give you your license details virtually immediately, with no need for waiting for me. Which of course means less work for me, too, so I can spend more time answering support questions or writing code.

Getting technical

Implementing the PayPal automation wasn't too difficult, though it took some research to find the best solutions. For the standard PayPal business accounts, they offer two automated payment notification services: Payment Data Transfer (PDT) and Instant Payment Notification (IPN). My initial research showed people using PDT, for example that's what the AquaticPrime framework uses. However, further research showed that this might not be the best choice. The way it works is to send the transaction information to your server when the customer is redirected back to your website after the purchase. But if they close their browser window or click away before they return to your site, your server doesn't get the transaction info. Also, you have to deal with pending transactions (e.g. eChecks), form reloads, and other issues.

So then I switched to using IPN. This is a more robust mechanism, but still quite simple to implement. In this case, the payment notification occurs in the background, whether or not the customer actually returns to your site. I believe that pending transactions aren't received until they clear, too. So this is more reliable and efficient.

Both services include fraud protections, in the form of posting the received data back to PayPal and getting a response. If they originated the data, they'll reply saying that it was confirmed, otherwise it's suspicious.

To make things more interesting, my store pages offer multiple products for sale, so I need a shopping cart. Most existing examples assumed a single buy button, but fortunately the PayPal data copes with shopping cart transactions in a fairly simple way, appending digits to the variables.

Continue reading part 2, with a discussion of the PHP code to implement this.

Syndicate content