PayPal Subscriptions Recently, I blogged about a PayPal Subscriptions project I was working on. This was a website where people could subscribe via PayPal for weekly religious lessons under one of three user categories. The previous post looked at the differences between the Web Site and Web Application Project models and what this meant when attempting to implement user profiles in conjunction with the membership system. Since I had previously created a few basic E-Commerce websites with regular Buy-Now buttons, I thought this would be a breeze... wrong :-|

As part of the project requirements, there would be separate, weekly messages displayed to the subscribers. The first message of a yearly subscription that would be displayed, would be tied to the original subscription date. No matter what kind of subscription system or periodical you are publishing, at some stage you will need to offer the correct issue number to a given subscriber. Since everyone will be subscribing at different times, this means keeping count! I decided to keep a tab of the week numbers. Bear in mind, that week calculations are done differently in different cultures and there is not a bug if you see week number 53!

using System.Globalization;

CultureInfo cultInfo = CultureInfo.CurrentCulture;
int weekNumNow = cultInfo.Calendar.GetWeekOfYear(DateTime.Now,
                                              cultInfo.DateTimeFormat.CalendarWeekRule,
                                              cultInfo.DateTimeFormat.FirstDayOfWeek);

 

For the task in hand, I used ASP.NET 2.0 and C# with VS 2005 and SQL Server 2005. It was built as a Web Application Project, which meant using the Web Profile Builder wrapper class to enable access to user profiles. I've heard of several people getting errors with this but it worked for me out of the box. I'm inclined to think that some may have overlooked a particular section of the documentation which I am going to repeat here:

"When your project has reloaded, you need to generate the web profile class and manually include it in your project. Use the solution explorer to do this. In the solution explorer, choose the “Show All Files” option and press the “Refresh” button two times. The first time will generate the profile class and WebProfileBuilder.user file. The second time will actually show the files now that they exist. To include the generated profile class, right-click on the generated profile class and choose “Include In Project”. Now you will be able to write code against the profile class."

The rest is pretty straightforward:

Web.Config Profile Section

 

public partial class MyClass : System.Web.UI.Page
{
    private WebProfile Profile
    {
        get
        {
            return new WebProfile(Context.Profile);
        }
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        string subscriberID = Profile.subscriberID;
        string subscriberGroup = Profile.subscriberGroup;
    }
    
    ...
}

 

Of course, the glue that holds the entire application together is the IPN class. I had previously used the freely-available IPN class from XDev Software. Note that this class is designed for regular PayPal transactions. I had to customize it to handle PayPal subscriptions. All you really have to do is pare it down to handle only the subscription-related variables you are handling on the return handshake from PayPal. The following set of return variables worked for my particular scenario; you will set these as properties of your custom IPN class:

        string txn_type;
        string business;
        string item_name;
        string payment_gross;
        string payment_status;
        string receiver_email;
        string payer_email;
        string pending_reason;
        string payment_date;
        string subscribe_id;
        string first_name;
        string last_name;
        string custom;

 

Here's an overview of a typical subscription Web application life-cycle:

1) User subscribes on your site and is directed to the PayPal site where they pay
2) Your IPN class handshakes with PayPal and grabs the values returned by PayPal
3) Your IPN class updates your application database and generates an email to the subscriber with a return URL to register on your site
4) Your IPN class generates an email (backup) to the merchant with the subscription data
5) The subscriber creates a user account on your site and you set up any extra member info you want to store in their profile

protected void Createuserwizard1_CreatingUser(object sender, EventArgs e)
{
    WebProfile p = WebProfile.GetProfile(CreateUserWizard1.UserName, true);
    p.Initialize(CreateUserWizard1.UserName, true);
            
    p.subscriberID = ViewState["SubscriberID"].ToString();
    p.subscriberGroup = ViewState["Group"].ToString();
            
    p.Save();
}

 

Some Gotchas:

Do not use a return URL from PayPal
Do not mix IPN and PDT. IPN is all you need
Do not forget the Save() method when creating the profile :-S




I recently posted a solution to the eternal PayPal / ASP.NET form submission problem using Jeremy Schneider's custom GhostForm class. Since then, several people have made mention of a problem that I came across myself when coding this, namely getting your project to recognize the reference to the new custom form class.

PayPal Checkout Button

 

Using a Web Application Project in VS 2005, I recently came up against something similar when attempting to place the SqlHelper.cs class in the App_Code folder. At that time I offered a quick hack. Since then, I have thought better of using the App_Code folder in my Web Application Projects and just create a normal folder and put the helper class in there along with my data access class. The App-Code is more trouble than it is worth for a small project where there is practically zero compilation time to be saved anyway.

Back to the problem at hand... when attempting to compile, you may get the following error:

"The name 'mainForm' does not exist in the current context"

First, check your scopes; make sure that wherever you are using the mainForm object is in the same scope as the instantiation. Ideally, create a separate Class Library Project in your solution and add the custom form class to it. Compile your new project separately and reference that from your e-commerce project. Right-click the References folder in Solution Explorer and browse to the DLL for the custom form.

CustomForm Class Library Project

 

Add the following to your master page and ignore any red squigglies you get in Visual Studio:

<%@ Register TagPrefix="CF" Namespace="CustomForm" Assembly="CustomForm" %>
<body>
    <CF:GhostForm id="mainForm" runat="server">
    ...
</body>

 

Add markup to the ASPX for the dummy PayPal button and a functioning ASP.NET button:

<img src="https://www.sandbox.paypal.com/en_US/i/btn/btn_xpressCheckout.gif"> <asp:Button ID="checkoutBtn" runat="server" OnClick="CheckButton_Click"
    Text="Checkout" Width="100" CausesValidation="false" /> 

 

using CustomForm;

namespace MyProject
{
    public partial class purchase : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            ...
            // Workaround for PayPal form problem
            GhostForm mainForm = new GhostForm();
            mainForm.RenderFormTag = false;
        }
        ...
    }
    ...
}

 

Although specific to my own project requirements, here's the complete handler code for the button click: 

        protected void CheckButton_Click(object sender, EventArgs e)
        {
            // Live PayPal URL
            // const string SERVER_URL = "https://www.paypal.com/cgi-bin/webscr";
            // Sandbox PayPal URL
            const string SERVER_URL = "https://www.sandbox.paypal.com/cgi-bin/webscr";
           
            // Live business parameter
            // const string BUSINESS = "grace@graceguitars.com";
            // Sandbox business parameter
            const string BUSINESS = "tester@hotmail.com";

            // Return URL for IPN processing
            const string NOTIFY_URL = "http://www.mysite.com/PayPalReturnURL.aspx";

            decimal totalAmount = 0.00M;
            int productID = 0;
            int totalUnits = 0;
            decimal totalShipping = 0.00M;
            string paypalURL = "";
            string itemName = "Grace\'s Guitars";
            string itemNumber = "";

            if (cart.Count > 0)
            {
                BizClass biz = new BizClass();
              
                // TransactionID will be later used to check against IPN info
                string transID = Guid.NewGuid().ToString();
               
                // Create a new Order in DB
                orderID = biz.AddOrder(out orderID, transID, false, DateTime.Now);
                itemNumber = Convert.ToString(orderID);

                foreach (ShoppingCartItem item in cart)
                {
                    totalAmount += item.Total;
                    totalUnits += item.Units;
                    productID += item.ProductID;

                    // Store order details in database
                    biz.AddOrderDetails(orderID, productID, item.Units);
                }   
                // Eventually, use a SQL Server job to remove unconfirmed orders

                // Calculate total shipping cost for total number of units
                totalShipping = CalculateShipping(totalUnits);

                // Get back the URL-encoded URL for PayPal   
                paypalURL = GetPayPalURL(SERVER_URL, BUSINESS, itemName, itemNumber,
                    totalAmount, totalShipping, NOTIFY_URL);
                Response.Redirect(paypalURL, true);
            }
        }

 

You need to sign into your PayPal Developer account before submitting your test purchases. You will be able to see a history of your test transactions in the sandbox admin section.

PayPal Sandbox

 

If you want some sample code for constructing the URL, I suggest you check out the following whitepaper from Rick Strahl. This should be enough to see you up and running. Many times, people get compiler errors due to badly-formed namespace declarations and class references. Always double-check your code :-)

 

GhostForm.zip (448.00 bytes)




Pondering... I recently started work on my first website to integrate with PayPal. The client needed it done relatively quickly. What started out for me as a mental picture of a products Web form with a "Buy Now" button, somehow turned into a full-blown e-commerce application complete with custom shopping cart. Talk about feature creep! And most of it was my own fault because I failed to anticipate the minimal requirements for the job. I'm still in the thick of it and have just implemented an admin back-end for the merchant to add products, complete with images, to the database. Then, I had to create an HttpHandler to stream the images...

They do say it's the simple things that get you stumped. For all of you ASP.NET developers out there who have yet to integrate a site with PayPal, just wait until you finally get to add that button to your form to pass the transaction details over to PayPal... In short, it won't work! The button HTML from the PayPal site is embedded in a form tag. You can only have one form on an ASP.NET page and ASP.NET provides its own. If you have a master page, then the form tag is in there and it is applied to every page in the site when they are merged with the master at runtime.

Thinking Irishman PayPal support does not offer a practical solution. They may try to get you to download their ASP.NET SDK which is 1.1 and uses Web Services. Most people have failed to get it to work with 2.0. Then they may tell you to put the form tag "outside" the main tag or on a separate HTML page, etc. I have seen endless hacks, most of which were too stupid to even consider; IFrames anyone?!!

I trudged through the forums and saw that ASP.NET developers have been asking how to get around this for the last three years or so. PayPal refuses to acknowledge the problem and seem more inclined to offer support for the PHP community. There is something radically wrong with this mindset from a business point of view.  Can PayPal not afford to pay some contractors to go in and develop an ASP.NET 2.0 SDK that will work with both NVP and Web Services? Nothing like speed to kill. Then PayPal had the temerity to invite me to complete a survey on how good I found their support service...

The Light Goes On So I turned to Google. I spent days concocting search strings that would bring that elusive nugget I needed to solve the problem. I thought I had found it when I came across the nested master page hack - keep the outer master page stripped of any form tag and then just use it for the page with the PayPal button. It would probably work, but if you don't get a code smell from that one, you may need to get your sinuses reamed out. The search continued. You know you're desperate when you start entering your grannie's middle name in the search query string :-O

Persistence finally paid off. I found an elegant solution on Jeremy Schneider's blog that consists of a custom HtmlForm class that can have the form tag rendering toggled on and off. The class is called GhostForm and has a property, RenderFormTag. When RenderFormTag is set to false, it doesn't render the opening or closing tags, but does render all of the contents. Reference the custom GhostForm class and in the code-behind of the form on which you are placing the button, place the following in the Page_Load to disable the master page form tag:

public partial class Products : System.Web.UI.Page
 {
     protected void Page_Load(object sender, EventArgs e)
     {
         GhostForm mainForm = new GhostForm();
         mainForm.RenderFormTag = false;
         .....     
     }
         // Send your data to PayPal :-)
     .....
 }