site

Personal vs professional

As you know, I publish this blog on the Dejal website.  It has been around for many years, and has discussed all kinds of topics.  Dejal product news, of course, plus general Dejal topics, and posts for other developers with open source and code tips.

Sometimes I post on more general topics, like Mac commentary and WWDC visits.

And I sometimes post about things going on in my life, like my travels, videos I find interesting or entertaining, stuff I find amusing, or my cats.

I've been wondering if the more personal topics are really appropriate for a business blog, though.  Looking around at other indie Mac and iOS company blogs, some do post random topics, but most stick to app update announcements, tips, and other core topics.

What do you think of this diverse mix of blog posts?  Do you like getting to know your app developers a little better, or would you prefer developer blogs kept to discussing their products?

Should I create a separate blog for non-Dejal-related topics?

What about the Dejal Twitter and Facebook accounts?  Should they stick to Dejal news too, or is general life stuff okay?  I'm not all that verbose with tweets, with often only a few tweets per day, sometimes none.  But the vast majority of the tweets aren't about Dejal topics.  Should I use a different account for personal tweets?  (I do have a personal Twitter account, @dejus, but don't currently use it.)

I'd appreciate your thoughts in the comments, via Twitter (@dejal), or privately.

Plus, I have a poll!  Take a moment to vote.

TrialPay Store

A few weeks ago I added a new payment processor to my site: TrialPay, which I mentioned in a previous blog post.

It's a great deal for my customers, as they can get automatic discounts when buying more than one app at the same time, or by try a third-party product that they might have been interested in anyway. And it's a great deal for me, as the discount offers cover the cost of the payment processing, so I get 100% of the money my customers pay for my apps, instead of losing a few percent to the payment processor.

I've been very satisfied with the service. When I first set it up, my contact at the company was extraordinarily helpful, with prompt replies to my queries, and doing much of the setup work for me, or providing plenty of info on the steps I had to do. I was quite impressed.

So I'd like to recommend their service to other Mac developers. It's a great way to offer quick-and-easy payment processing that gives your customers a great deal.

Here's my referral link. If you use this to sign up as a TrialPay merchant, I'll get a $500 referral bonus, and you'll get a $250 Amazon gift card by way of thanks. More details on their site.

TrialPay Referral Program

Dejal site tweaks

Over the weekend I made a few changes to the Dejal website.

One was somewhat trivial on the surface, though required updating several pages: I increased the page width from 900 pixels to 980 pixels, which is the same width Apple and others use. 80 pixels doesn't sound like much, but it gives a little more room for page content. If things look wacky, try reloading; you may have a cached CSS file from a previous visit.

The wider page also gave room for a new tab in the page header: iPad. This new page lists my vast collection of iPad software... namely Tweeps. Though as I write this, the iPad edition of Tweeps isn't released yet, but this was in preparation for that momentous occasion.

I do plan on adding more iPad apps in the future: I want to do iPad editions of at least two of my Mac apps, Simon and Caboodle. I probably won't have time to implement either this year, but they'll come eventually.

Similarly, I added an iPad block to the sidebar, between the Mac and iPhone ones, to make it really easy to see what apps are available for each platform.

Another change was the introduction of a new TrialPay-powered Dejal Store, using their new e-commerce solution. I have made that the default store (for most Mac apps), as it has some advantages over the PayPal-based store. For customers, it has the ability to give automatic discounts when you buy more than one of my apps. Plus you can get a big discount by accepting a third-party offer — for something you might be wanting to buy anyway. And it benefits me, as there are no transaction fees — the discount offers pay for the service. Try it, and tell me what you think.

I already had an experimental TrialPay store before, which has now been expanded to the same set of Mac apps. This is now called the Get It Free store. This is similar to getting a discount when outright buying one of my apps, except that you get the app completely free! Here's how it works:

Take a look and get an app for free!

I have a few more changes planned. One is to add some smaller feature boxes to the home page. Currently, there's a single large feature graphic at the top of the page, highlighting the latest significant release, but I'd like to be able to highlight other recent updates of note, and make it easier to see what apps I offer. I've experimented with mockups of a few designs, but this is my current favorite:

What do you think?

I've also been debating on whether or not I should keep the latest blog post on the home page. I kinda like it, as it makes it easier for people to see, but some blog posts perhaps don't really belong in such a prominent position.

So, I posted my first poll last night, asking how people read the Dejal Blog — via the RSS feed, Twitter mentions, on the Blog page, on the home page, etc. Please take a moment to cast your voie in the poll. (If you saw it yesterday and weren't able to vote, try again; it is now open to anonymous voters.)

Developers should iPhone-optimize their sites

A while ago I wrote how the Dejal site is iPhone-optimized: when you view it on an iPhone or iPod touch, the website content is reformatted to fit neatly in the 320-pixel-wide display:

I would suggest that any developers who write iPhone software should do this too. So here's some technical info on what I did. This isn't necessarily the best solution, but it works for me, and isn't very difficult.

Firstly, of course, you need to be able to detect whether you're running on an iPhone or elsewhere. The standard way to do this is by looking at the "user agent" value of the HTTP session. In PHP, you can simply look at the $_SERVER['HTTP_USER_AGENT'] global variable. I have the following function in a utility PHP file included on every page (via the header code):

    function getIsIPhonePlatform()
    {
        global $private_is_iphone_platform;
        
        if (isset($private_is_iphone_platform))
            return $private_is_iphone_platform;
        
        $user_agent = $_SERVER['HTTP_USER_AGENT'];
        $private_is_iphone_platform = stristr($user_agent, 'iPhone') || 
            stristr($user_agent, 'iPod') ||
            stristr($_GET['platform'], 'iPhone');
        
        return $private_is_iphone_platform;
    }

This function returns whether or not the user agent is an iPhone or iPod touch, either by returning the state if already known, or determining it if not. It also allows testing the iPhone-optimized pages from your Mac by adding "platform=iphone" to a page's URL parameters (try it; it's fun!).

Not everyone would want the pages optimized, though: a great thing about the iPhone is MobileSafari does an excellent job of rendering "real" web pages. So I also added a checkbox at the bottom of every page to toggle iPhone-optimized mode off and on. The state is recorded in cookie. So I have another function to read that, on the iPhone platform:

    function getIsIPhoneOptimized()
    {
        global $private_is_iphone_optimized;
        
        if (isset($private_is_iphone_optimized))
            return $private_is_iphone_optimized;
        
        $private_is_iphone_optimized = getIsIPhonePlatform();
        
        if ($private_is_iphone_optimized && isset($_COOKIE['iphone_optimized']))
            $private_is_iphone_optimized = $_COOKIE['iphone_optimized'];
       
        return $private_is_iphone_optimized;
    }
Then the checkbox is actually output (in the footer's include file) via somewhat messy code that uses PHP to output the JavaScript to set the cookie and reload the page when the checkbox is toggled, and the checkbox itself:
    function outputIPhoneOptimizationCheckbox()
    {
        if (getIsIPhonePlatform())
        {
            $query_params = $_SERVER['QUERY_STRING'];
            
            if ($query_params != '')
                $query_params = '?' . $query_params;
            
            echo('<script type="text/javascript">' . "\n");
            echo('<!--' . "\n\n");
            echo('function iphoneOptimizedToggled()' . "\n");
            echo('{' . "\n");
            echo('  document.cookie=\'iphone_optimized=' .
                !getIsIPhoneOptimized() . '; path=/\';' . "\n");
            echo('  window.location.reload(true);' . "\n");
            echo('}' . "\n\n");
            echo('-->' . "\n");
            echo('</script>' . "\n\n");
            
            echo('<p><input type="checkbox" id="iphone_optimized_checkbox"
                onclick="iphoneOptimizedToggled()"');
            
            if (getIsIPhoneOptimized())
                echo(' checked="checked"');
            
            echo(' /><span id="iphone_optimized_label"
                onclick="iphoneOptimizedToggled()">
                Display site optimized for iPhone</span></input>' . "\n");
            echo('</p>');
        }
    }
And finally, the getIsIPhoneOptimized() function is called in the header to use iPhone-optimized or normal style sheets. This is actually a simplification; it actually uses several style sheets, including some common ones and some platform-dependent ones. It also sets the viewport appropriately for each platform — that is a key aspect for iPhone optimization:
    if (getIsIPhoneOptimized())
    {
        echo('<link rel="stylesheet" href="/iphone/header.css"
            type="text/css" media="all" />' . "\n");
        echo('<meta name="viewport"
            content="width=device-width, user-scalable=no" />' . "\n");
    }
    else
    {
        echo('<link rel="stylesheet" href="/mac/header.css"
            type="text/css" media="all" />' . "\n");
        echo('<meta name="viewport" content="width=900" />' . "\n");
    }
Of course, configuring the CSS appropriately is another story, but not too difficult... and very site-specific. Feel free to explore my CSS files if desired: I hope this is helpful. There are a number of other aspects, like fitting images in the available space, supporting movies that can play on the iPhone, and more. If there's interest, I might write more about this in the future.

Dejal site now iPhone optimized

Yesterday I talked briefly about the updated Dejal website design, but I didn't mention the biggest change: the whole site is now displayed in an optimized state when viewed on an iPhone or iPod touch.

When viewed there, it will use a simplified header with just the Dejal logo, plus special menus similar to iPhone-native ones, and will display the sidebar content after the main body content. It also fits the text to the screen, adjusts image sizes if too large, and other optimizations:

So how do you access the other pages? Simply tap the Dejal logo to display a special menu page, that includes the items from the normal page header in an iPhone-friendly menu. The Mac and iPhone pages are also displayed more simply, too.

There is also a checkbox at the bottom of every page to toggle iPhone-optimized mode off and on (as you can see in the above picture). By default it is on, but if you turn it off the page will change to use the same layout as on your Mac or PC, where you can pinch to zoom etc as normal:

One thing worth mentioning: since the Forum etc tables are too wide to fit, they now scroll horizontally. There's no real need, but if you want to see obscured columns you can use two fingers to scroll horizontally:

I hope you enjoy the changes! Again, please let me know if you notice any issues with any aspect of the new website design.

Updated website design

Over the last couple of weeks I've been tweaking the Dejal website design. Nothing too radical, but some visible changes, and a number of behind-the-scenes changes too. The redesign is now live.

The most visible change is the page header: it no longer has the app icons. Instead, it has new Mac and iPhone items, which lead to pages of those products. The header also looks more modern than it did.

The reason for moving the product icons out of the header was simply that it was running out of space. I recently released my first iPhone app, SmileDial, and will be releasing more new apps this year.

The apps aren't gone completely, though: I've added them to two boxes in the sidebar, one for Mac apps and the other for iPhone apps. The Mac box includes an item for my free stuff, too.

On the product pages, I've made several more changes. The product icon, name and slogan are now displayed above the body content, and the submenu of product pages is drawn differently. Plus the buy/download links in the sidebar are drawn differently, with boxes for beta and older versions as well as the current general release, where appropriate.

I'm still tweaking a few aspects of the site design, but it's mostly done. If you notice any broken links, overlapping or incorrectly sized blocks, or other things that don't look right, please let me know.

Dejal year in review: 2008

The year 2008 was another interesting year for Dejal. Here are some highlights:

Simon: My flagship website and server monitoring tool had one significant update, version 2.4, with a couple of fix updates bringing it to 2.4.2. These versions added the Twitter and Calendar plug-ins, plus enhancements to several others, and lots of other improvements. What's next? Version 2.5 is currently in development, and will have the first beta release shortly. It includes a significant new SMS notifier, plus other enhancements.

Time Out: My handy break reminder tool was updated to version 1.5.2, plus some work was done on version 2. I had hoped to get version 2 released in 2008, but it got postponed due to some other projects. What's next? Version 2 is still coming, probably around May 2009.

Caboodle: This handy snippet-keeper app was updated to version 1.2. This release included several encryption enhancements and other improvements. What's next? I have a long list of ideas for Caboodle. It should see several significant enhancements in 2009.

Narrator: My fun speech synthesis app had a major upgrade and rewrite using Leopard technologies, plus some fix releases bringing it to version 2.0.3 currently. This major upgrade was much deserved, since the previous release was back in 2003! I also started experimenting with giving away Narrator licenses via TrialPay. What's next? Narrator 2.1 will be released before the end of the year, with fix releases before that.

BlogAssist: This useful HTML markup tool was updated to version 2.2, with 2.2.1 the latest release. It added much-requested repeating formatting, and other changes. What's next? I have several ideas for BlogAssist, too, with several updates planned.

Macfilink: My affiliate link cloaking app was updated to version 1.5 and released as freeware. This was a tricky decision, but I still feel that it was the right one. I don't currently plan on any further improvements to it, as it does it's one job very well, but will do bug-fix releases as needed.

SmileDial: I released my first apps for iPhone in 2008. SmileDial Lite and SmileDial Pro are innovative apps using a visual address book for your favorite people, to make it easy to text and call one or more people. What's next? A minor release will be done soon, with some feature enhancements planned.

What else will 2009 bring? I'm about to start a new Mac app with a companion iPhone app as a joint venture with another developer. I want to write at least one other new iPhone app, too. But I don't want to push back updates to my existing apps more than necessary. I'm also currently working on some updates to the website, including optimizations when viewing on an iPhone, which I'll roll out soon.

I'm looking forward to another great year. Thank you to all of my customers who have helped support Dejal.

Email back up

My email problem has been corrected. I'll catch up on the email backlog soon.

So you can email me (or use the feedback form) if desired. But the Dejal Forums is still the preferred way to get support, so my customers can help each other, and everyone can benefit from my and others answers. As I said, Simon notifies me when changes occur there, so I'm able to respond quickly.

My email is not working

Due to a technical problem at my web host, I can not currently receive incoming email. So feedback sent via either direct email or the Feedback form won't reach me until this is fixed.

In the meantime, please post support queries to the Dejal Forums. Simon will notify me when changes occur there, so I'll be able to respond quickly, as always. The forums are the preferred support channel anyway, so my customers can help each other, and everyone can benefit from my and others answers.

Purchases are not affected; they are handled fully automatically by my server.

Sorry for any inconvenience! Hopefully the issue will be resolved soon.

Added a Dejal products page

I've just added a Dejal Products page to the site.

This page serves as a quick summary of all current applications available from Dejal, plus links to the older applications and tools that I keep available for those who want them.

I imagine I'll use it for brief lists of my apps, as often appears on third-party promotion pages, e.g.:

Dejal produces Simon, a website/server monitoring tool, Time Out, a free break reminder, Caboodle, a snippet keeper, Narrator, to read stories in multiple voices, and more.

Get Narrator for free via TrialPay

[TrialPay]I have added a new payment option to the Dejal Store: TrialPay.

This is an interesting concept. Instead of directly buying one of my products, you try or buy one of the many products offered via the TrialPay service, and get my product for free as part of the bargain. They have many products available, including ones you'd likely buy anyway, such as eBay, Blockbuster, Stamps.com, and hundreds of others.

In fact, with Valentine's Day coming up, now's a perfect time to use FTD.com for flowers and get Narrator as a free bonus!

How TrialPay works:

Initially I am only offering a Household license for Narrator via TrialPay. Based on my experience with how well it works, and feedback from my customers, I may extend it to all of my products. But for now, you can get Narrator for any number of people in your home for free — a $24 value!

To take advantage of this deal, click this button to begin:

Please help me evaluate this by contacting me if you have any thoughts on TrialPay - whether you choose to use it or not.

If you're a developer and want to find out how TrialPay can help you, you can find out more information on the TrialPay merchant site.

Featured blog posts of 2007

My blog posts often just cover new releases, but sometimes I post general-interest or developer-interest topics. Some highlights from 2007 included:

I hope you enjoyed these posts.

Dejal year in review: 2007

The year 2007 was a good one for Dejal, with lots of releases, nicely growing revenue, and other goodness. Here are some highlights from the year:

LeopardApple released Mac OS X 10.5 "Leopard" in October. It is an excellent upgrade, with hundreds of enhancements, both at the UI level and in terms of frameworks and tools for developers.

Simon: My flagship website and server monitoring tool had two significant updates, versions 2.2 and 2.3, with several fix updates bringing it to 2.3.5. These versions added the versatile Script service and notifier, plus several other plug-ins and lots of other improvements.

Time Out: Version 1.5 of this popular freeware break reminder app was released, with 1.5.1 as the latest release as of this writing. This version added a much improved icon, layout tweaks, and other changes.

Caboodle: This handy snippet-keeper app was updated to version 1.1, with the current release being 1.1.3. These releases added import and export functions, enhanced printing, and many other improvements.

Narrator: My fun speech synthesis app hasn't been updated for a number of years, but I've been working on a rewrite of it using Leopard technologies, which is currently in private alpha testing, and will be available publicly soon.

BlogAssist: This useful HTML markup tool was updated to version 2.1, with 2.1.2 the latest release. It added a very useful Services menu command, allowing inline replacement of text -- something I use daily. Plus other changes.

Macfilink: My affiliate link cloaking app was updated to version 1.4, with 1.4.2 the latest currently. It didn't have many changes, since it is a very simple app that does one job well, but it had some tweaks.

Dejal site: The Dejal web site also had a number of improvements in 2007. Back in January, the entire site was migrated to a new server, driven by a hybrid of custom PHP code and the Drupal CMS. It uses my custom code for the product and store pages, among others, and Drupal for the remainder -- basically any page with a navigation menu or login fields in the sidebar is a Drupal-powered page. Using Drupal allows easy blog posting, forums, FAQs, plus combined account management and commenting. I also added a Developer section, where I share some of my Cocoa code for other Mac developers. The screenshot slideshows were also improved, using JavaScript instead of reloading the page each time, and I added JavaScript to the Store pages to make them prettier. Speaking of the Store, I also implemented PayPal's Instant Payment Notification service, allowing purchases to be processed promptly and automatically.

2007 was also an interesting year in terms of Mac events. I attended Macworld for the first time (as an attendee only, not an exhibitor), which was quite interesting. I also attended my second WWDC.

What will 2008 bring? I don't want to pre-announce too much, but you can expect updates to all of my current apps, some of them major upgrades with significant new looks. One such is Time Out version 2, which I'm really excited about. Another is Narrator 2, which will be going into public beta testing very soon.

Having RSS issues

I'm having issues with my RSS feed. Sorry about that. I think I've fixed it, so this is a post to see if it's working now, as well as to apologize for the issues.

I think it happened as a result an update to the Drupal CMS I use.

Possible Dejal site downtime this week

The Dejal site will be moving to a new server at a new data center sometime this week, as part of a planned migration that my web host is performing. Hopefully this will result in improved reliability and performance of the server. But in the meantime, the site may be offline for a while.

They assure me that there shouldn't be any noticeable downtime, or only for a few minutes. But you never know with such operations, so I thought I'd provide warning here. Apologies in advance if you try to access the Dejal site and get an error. Please just try again later if you do.

Rest assured that I will be closely monitoring the situation (via Dejal Simon, of course!), and will do what I can to minimize any downtime.

Tweet tweet tweet

You may have heard of this little thing called Twitter on the intertubes. It's a site where people can post short (140 characters or less) updates on what they're doing, which can be viewed on the web, cellphones, etc. People can "follow" others they're interested in, and others can follow what you're up to. You know, one of those social networking thingies.

I've resisted the siren song of Twitter for quite a while, since I've been somewhat dubious about the value of such short updates, but it does seem quite popular, so I've finally given in. I've enjoyed reading about events in the lives of other Mac developers and Mac bloggers, and it's been useful to get a feel for the C4[1] conference last weekend, which I wasn't able to attend, unfortunately.

You can now see what I'm up to via my Twitter page. We'll see how it goes.

Commenting now requires login

I've resisted this move as long as possible, but commenting on Dejal blog entries, forums, etc now requires logging in. I tried using a captcha, and back-end spam detection software, and an approval queue, but while each of those helped keep the spam off the site, I've still had to wade through hundreds of spam comments.

I've noticed that most people seem happy to create an account on the site anyway, so this step shouldn't affect too many people. Creating an account is free and easy, and allows much easier access for subsequent comments, allows posting new forum topics, and allows tracking posts so you're notified when there's a reply, if you so desire.

If you don't want to create an account, you can always contact me privately instead.

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