diff --git a/modules/recurlyjs/README.txt b/modules/recurlyjs/README.txt
new file mode 100644
index 0000000..e441a41
--- /dev/null
+++ b/modules/recurlyjs/README.txt
@@ -0,0 +1,67 @@
+Description
+-----------
+This module provides FormAPI elements for subscriptions, updating billing
+information, and one-time payments through Recurly.com.
+
+EXTREMELY IMPORTANT CAVEAT: While this module provides convienent FormAPI
+elements such as $element['#type'] = 'recurlyjs_subscribe', these elements
+operate entirely through JavaScript and sensitive information never touches
+your server (thus circumventing PCI compliance requirements). This comes with
+the following caveats:
+
+- The recurlyjs elements cannot be on the same page as other elements that need
+  validation, because the credit card will be charged client-side. Validation
+  errors done server-side would be *after* the credit card has been charged.
+  It is recommended to build forms that only contain the recurly element, or
+  use multiple-step forms with only the recurly element on the last page by
+  itself.
+- Users must have JavaScript enabled in their browser to submit payment.
+- RecurlyJS is compatible with IE7 and higher.
+- RecurlyJS recommends jQuery 1.5.2 or higher. Drupal 7 comes with jQuery 1.4.4
+  out of the box, however we are not aware of any issues with using RecurlyJS
+  with this older version of jQuery.
+
+Requirements
+------------
+Drupal 7.x
+Recurly
+Libraries API
+
+API Information
+---------------
+This module provides 3 FormAPI elements that help manage credit cards through
+the Recurly service.
+
+Subscription forms:
+
+$form['subscribe'] = array(
+  '#type' => 'recurlyjs_subscribe',
+  '#plan_code' => 'gold',
+);
+
+Billing update forms:
+
+$form['billing_update'] = array(
+  '#type' => 'recurlyjs_update',
+  '#account_code' => 'hash',
+);
+
+And one-time payment forms:
+
+$form['subscribe'] = array(
+  '#type' => 'recurlyjs_payment',
+  '#amount' => 3000,
+  '#description' => t('Description to show up on invoice'),
+);
+
+There are several other options available for each type, but these basics are
+the most common. For more information see the recurlyjs_element_info() function
+in the recurlyjs.module file.
+
+Support
+-------
+Please use the issue queue for filing bugs with this module at
+http://drupal.org/project/issues/recurly?categories=All
+
+WE DO NOT PROVIDE SUPPORT ON CUSTOM CODING. Please use the issue queue for
+issues that are believed to be bugs or for requesting features.
diff --git a/modules/recurlyjs/css/recurly-widget.css b/modules/recurlyjs/css/recurly-widget.css
new file mode 100644
index 0000000..5e5986d
--- /dev/null
+++ b/modules/recurlyjs/css/recurly-widget.css
@@ -0,0 +1,687 @@
+/**
+ * @file
+ * Styling of the Recurly payment widgets. Used for new subscriptions, one-time
+ * payments and billing information updates.
+ *
+ * This CSS is based upon the RecurlyJS default theme, but with overly stylistic
+ * elements removed so as to make a more useful set of defaults that may be
+ * added upon.
+ */
+.recurly {
+  display: block;
+  position: relative;
+}
+.recurly .cost,
+.recurly .discount {
+  font-size: 16px;
+  text-align: right;
+}
+.recurly .subscription {
+  border-radius: 9px 9px 0 0;
+  text-shadow: 0 1px 0 #fff;
+  padding-top: 20px;
+  overflow: hidden;
+}
+.recurly .plan {
+  color: #333;
+  overflow: hidden;
+  position: relative;
+  zoom: 1;
+}
+.recurly .plan .name {
+  float: left;
+  font-size: 32px;
+  min-width: 200px;
+}
+.recurly .plan .quantity.field {
+  clear: none;
+  width: 60px;
+  margin: 4px 0;
+}
+.recurly .plan .quantity.field input[type=text] {
+  width: 48px;
+}
+.recurly .plan .quantity.field:before {
+  content: "\d7";
+  height: 48px;
+  line-height: 30px;
+  position: absolute;
+  right: 100%;
+  width: 40px;
+  font-size: 20px;
+  text-align: center;
+  vertical-align: middle;
+  z-index: 1337;
+  color: #666;
+}
+.recurly .plan .recurring_cost {
+  float: right;
+  text-align: right;
+  padding-right: 20px;
+}
+.recurly .plan .recurring_cost .cost {
+  font-size: 32px;
+}
+.recurly .plan .recurring_cost .interval {
+  font-size: 12px;
+  padding-bottom: 20px;
+}
+.recurly .free_trial {
+  clear: left;
+  float: left;
+  font-size: 13px;
+  height: 22px;
+  margin: 0;
+  position: absolute;
+  top: 35px;
+  left: 20px;
+  font-style: italic;
+}
+.recurly .setup_fee {
+  clear: both;
+  border-top: 1px #CCC dashed;
+  overflow: hidden;
+  padding-top: 20px;
+}
+.recurly .setup_fee .title {
+  float: left;
+  padding-left: 20px;
+  font-weight: bold;
+  font-size: 16px;
+}
+.recurly .setup_fee .cost {
+  float: right;
+  padding-right: 20px;
+}
+.recurly .vat {
+  height: 24px;
+  padding: 20px 20px;
+  display: none;
+  border-top: 1px #CCC dashed;
+}
+.recurly .vat.applicable {
+  display: block;
+}
+.recurly .vat .title {
+  font-size: 16px;
+  font-weight: normal;
+  float: left;
+}
+.recurly .vat .cost {
+  float: right;
+  font-size: 18px;
+}
+.recurly .add_ons {
+  clear: both;
+}
+.recurly .add_ons.any {
+  margin: 20px 0;
+}
+.recurly .add_ons .add_on {
+  background: #ecedee;
+  background: -webkit-linear-gradient(top, #ecedee, #e5e6e7);
+  background: -moz-linear-gradient(top, #ecedee, #e5e6e7);
+  background: -o-linear-gradient(top, #ecedee, #e5e6e7);
+  background: linear-gradient(top, #ecedee, #e5e6e7);
+  margin: 0;
+  height: 43px;
+  line-height: 42px;
+  vertical-align: middle;
+  position: relative;
+  clear: both;
+  overflow: hidden;
+  border-top: 1px solid #ccc;
+  border-left: 1px solid #ccc;
+  border-right: 1px solid #ccc;
+  text-shadow: 0 1px 0 #fff;
+  color: #999;
+  font-weight: 300;
+  font-size: 16px;
+  zoom: 1;
+  cursor: default;
+}
+.recurly .add_ons .add_on.first {
+  border-top-left-radius: 10px;
+  border-top-right-radius: 10px;
+}
+.recurly .add_ons .add_on.last {
+  border-bottom: 1px solid #ccc;
+  border-bottom-left-radius: 10px;
+  border-bottom-right-radius: 10px;
+}
+.recurly .add_ons .add_on .name {
+  font-size: inherit;
+  font-weight: inherit;
+  font-style: italic;
+  color: inherit;
+  width: 200px;
+  margin-left: 9px;
+  margin-right: 20px;
+  position: absolute;
+  left: 0;
+  top: 0;
+}
+.recurly .add_ons .add_on .quantity.field {
+  position: absolute;
+  top: 4px;
+  left: 249px;
+  width: 60px;
+  display: none;
+}
+.recurly .add_ons .add_on .quantity.field input[type=text] {
+  width: 48px;
+}
+.recurly .add_ons .add_on .quantity.field:before {
+  content: "\d7";
+  height: 48px;
+  line-height: 30px;
+  position: absolute;
+  right: 100%;
+  width: 40px;
+  font-size: 20px;
+  text-align: center;
+  vertical-align: middle;
+  z-index: 1337;
+  color: #666;
+}
+.recurly .add_ons .add_on .cost {
+  font-size: inherit;
+  line-height: inherit;
+  vertical-align: middle;
+  position: absolute;
+  right: 10px;
+}
+.recurly .add_ons .add_on:hover {
+  background: -webkit-linear-gradient(top, #f0f0f0 0%, #dfdfdf 50%, #d5d5d5 50%, #e0e0e0 100%);
+  background: -moz-linear-gradient(top, #f0f0f0 0%, #dfdfdf 50%, #d5d5d5 50%, #e0e0e0 100%);
+  background: -o-linear-gradient(top, #f0f0f0 0%, #dfdfdf 50%, #d5d5d5 50%, #e0e0e0 100%);
+  background: linear-gradient(top, #f0f0f0 0%, #dfdfdf 50%, #d5d5d5 50%, #e0e0e0 100%);
+  box-shadow: inset 0 1px 0 #fff;
+  text-shadow: none;
+  color: #111;
+}
+.recurly .add_ons .add_on:active,
+.recurly .add_ons .add_on.selected {
+  color: #111;
+  background: -webkit-linear-gradient(top, #f0f0f0, #fff);
+  background: -moz-linear-gradient(top, #f0f0f0, #fff);
+  background: -o-linear-gradient(top, #f0f0f0, #fff);
+  background: linear-gradient(top, #f0f0f0, #fff);
+  width: auto;
+  box-shadow: inset 0 1px 4px 0 rgba(0,0,0,0.07);
+  text-shadow: none;
+}
+.recurly .add_ons .add_on.selected {
+  background: #fff url("../images/check.png") no-repeat 10px center;
+}
+.recurly .add_ons .add_on.selected .name {
+  padding-left: 24px;
+}
+.recurly .add_ons .add_on.selected:hover {
+  background: #fcf5f0 url("../images/uncheck.png") no-repeat 10px center;
+}
+.recurly .add_ons .add_on.selected .quantity {
+  display: block;
+}
+.recurly .coupon {
+  clear: both;
+  overflow: hidden;
+  height: 34px;
+  color: #333;
+  position: relative;
+  border-top: 1px #CCC dashed;
+  padding-top: 12px;
+}
+.recurly .coupon .check {
+  width: 26px;
+  height: 26px;
+  float: left;
+  border-radius: 15px 15px 15px 15px;
+  background: #70ccf8;
+  border: 1px solid #0090c9;
+  margin: 3px 0 1px 10px;
+  box-shadow: inset 0 1px 0 0 rgba(255,255,255,0.35), 0 1px 1px 0 rgba(0,0,0,0.10);
+  background: #43bef9 url("../images/coupon_check.png") no-repeat center center;
+  background: url("../images/coupon_check.png") no-repeat center center, -webkit-linear-gradient(top, #71CDFA 0%, #43BEF9 50%, #00B1F6 50%, #71CEFB 100%);
+  background: url("../images/coupon_check.png") no-repeat center center, -moz-linear-gradient(top, #71CDFA 0%, #43BEF9 50%, #00B1F6 50%, #71CEFB 100%);
+  background: url("../images/coupon_check.png") no-repeat center center, -o-linear-gradient(top, #71CDFA 0%, #43BEF9 50%, #00B1F6 50%, #71CEFB 100%);
+  background: url("../images/coupon_check.png") no-repeat center center linear-gradient(top, #71CDFA 0%, #43BEF9 50%, #00B1F6 50%, #71CEFB 100%);
+}
+.recurly .coupon .check:hover {
+  background: url("../images/coupon_check.png") no-repeat center center, -webkit-linear-gradient(top, #71CDFA 0%, #43BEF9 50%, #00B1F6 50%, #71CEFB 100%);
+  background: url("../images/coupon_check.png") no-repeat center center, -moz-linear-gradient(top, #71CDFA 0%, #43BEF9 50%, #00B1F6 50%, #71CEFB 100%);
+  background: url("../images/coupon_check.png") no-repeat center center, -o-linear-gradient(top, #71CDFA 0%, #43BEF9 50%, #00B1F6 50%, #71CEFB 100%);
+  background: url("../images/coupon_check.png") no-repeat center center linear-gradient(top, #71CDFA 0%, #43BEF9 50%, #00B1F6 50%, #71CEFB 100%);
+  box-shadow: inset 0 1px 0 0 rgba(255,255,255,0.75), 0 1px 1px 0 rgba(0,0,0,0.10);
+}
+.recurly .coupon .check:active {
+  background: url("../images/coupon_check.png") no-repeat center center, -webkit-linear-gradient(top, #f0f0f0, #fff);
+  background: url("../images/coupon_check.png") no-repeat center center, -moz-linear-gradient(top, #f0f0f0, #fff);
+  background: url("../images/coupon_check.png") no-repeat center center, -o-linear-gradient(top, #f0f0f0, #fff);
+  background: url("../images/coupon_check.png") no-repeat center center linear-gradient(top, #f0f0f0, #fff);
+  box-shadow: inset 0 3px 3px 0 rgba(0,0,0,0.03);
+  border: 1px solid #999;
+}
+.recurly .coupon.checking .check {
+  background: #f0f0f0 url("../images/coupon_checking.gif") no-repeat center center;
+  box-shadow: inset 0 3px 3px 0 rgba(0,0,0,0.03);
+  border: 1px solid #999;
+}
+.recurly .coupon.invalid .coupon_code {
+  border-color: #a55;
+  background: #fee;
+  color: #311;
+}
+.recurly .coupon .coupon_code .error {
+  left: 300px;
+}
+.recurly .coupon .description {
+  float: left;
+  margin-left: 20px;
+  height: 34px;
+  line-height: 34px;
+  vertical-align: middle;
+  font-size: 14.4px;
+}
+.recurly .coupon .discount {
+  float: right;
+  height: 34px;
+  line-height: 34px;
+  vertical-align: middle;
+}
+.recurly .error {
+  padding: 5px;
+  line-height: 22px;
+  vertical-align: middle;
+  color: #000;
+  text-shadow: 0 1px 0 #fec;
+  background: #ffc;
+  border: 1px solid #ba1;
+  box-shadow: 3px 5px 5px 0 rgba(0,0,0,0.10);
+  border-radius: 5px;
+  font-size: 13px;
+}
+.recurly .server_errors {
+  color: #fff;
+  text-shadow: 0 1px 0 #000;
+  margin: 0 20px;
+  opacity: 0;
+}
+.recurly .server_errors .error {
+  padding-left: 26px;
+  background: rgba(240,250,0,0.50) url("../images/error.png") no-repeat 5px 9px;
+}
+.recurly .server_errors.any {
+  opacity: 1;
+  -webkit-transition: opacity 0.5s linear;
+  -moz-transition: opacity 0.5s linear;
+  margin: 20px 20px;
+  margin-bottom: 0;
+}
+.recurly .contact_info,
+.recurly .billing_info,
+.recurly .accept_tos {
+  position: relative;
+  overflow: hidden;
+  zoom: 1;
+}
+.recurly .contact_info .title,
+.recurly .billing_info .title,
+.recurly .accept_tos .title {
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 20px;
+  color: #404041;
+  text-shadow: 0 1px 0 #fff;
+}
+.recurly .contact_info .credit_card,
+.recurly .billing_info .credit_card,
+.recurly .accept_tos .credit_card {
+  clear: both;
+}
+.recurly .contact_info {
+  border-bottom: 1px #CCC dashed;
+  margin-bottom: 20px;
+}
+.recurly .accept_tos {
+  border-top: 1px #CCC dashed;
+  overflow: visible;
+}
+.recurly .accept_tos input[type=checkbox] {
+  display: inline;
+  line-height: 34px;
+  vertical-align: middle;
+}
+.recurly .accept_tos label {
+  margin: 0 0 0 5px;
+  display: inline;
+  line-height: 34px;
+  vertical-align: middle;
+}
+.recurly .accept_tos .field .error {
+  display: block;
+  position: static;
+}
+.recurly .field {
+  display: inline;
+  float: left;
+  clear: left;
+  width: 300px;
+  position: relative;
+}
+.recurly .field input[type=text] {
+  width: 288px;
+}
+.recurly .field.company_name {
+  margin-bottom: 0;
+}
+.recurly .field .error {
+  min-width: 128px;
+  white-space: nowrap;
+  position: absolute;
+  top: 0;
+  left: 100%;
+  margin-left: 20px;
+  z-index: 1337;
+}
+.recurly .field .placeholder {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 6px 6px 6px 8px;
+  line-height: normal;
+  font-weight: normal;
+  vertical-align: middle;
+  color: #999;
+  cursor: text;
+  overflow: hidden;
+  white-space: nowrap;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -o-user-select: none;
+  user-select: none;
+  font-weight: 300;
+}
+.recurly .field.focus .placeholder {
+  color: #ccc;
+}
+.recurly .field.invalid .placeholder {
+  color: #a77;
+}
+.recurly .field.coupon_code {
+  width: 140px;
+}
+.recurly .field.coupon_code input[type=text] {
+  width: 128px;
+}
+.recurly .field.first_name {
+  clear: left;
+  width: 140px;
+}
+.recurly .field.first_name input[type=text] {
+  width: 128px;
+}
+.recurly .field.first_name .error {
+  left: 300px;
+}
+.recurly .field.last_name {
+  width: 140px;
+  margin-left: 20px;
+  margin-left: 20px;
+  clear: none;
+  clear: none;
+}
+.recurly .field.last_name input[type=text] {
+  width: 128px;
+}
+.recurly .field.card_number {
+  width: 220px;
+}
+.recurly .field.card_number input[type=text] {
+  width: 208px;
+}
+.recurly .field.card_number .error {
+  left: 300px;
+}
+.recurly .field.cvv {
+  width: 60px;
+  margin-left: 20px;
+  margin-left: 20px;
+  clear: none;
+  clear: none;
+}
+.recurly .field.cvv input[type=text] {
+  width: 48px;
+}
+.recurly .field.expires {
+  width: 300px;
+}
+.recurly .field.expires input[type=text] {
+  width: 288px;
+}
+.recurly .field.expires .title {
+  float: left;
+  font-size: 13px;
+  line-height: 24px;
+  vertical-align: middle;
+  width: 59px;
+}
+.recurly .field.expires .month {
+  float: left;
+  width: 120px;
+  margin-left: 0;
+}
+.recurly .field.expires .month input[type=text] {
+  width: 108px;
+}
+.recurly .field.expires .year {
+  float: left;
+  margin-left: 1px;
+  width: 60px;
+}
+.recurly .field.expires .year input[type=text] {
+  width: 48px;
+}
+.recurly .field.state {
+  width: 180px;
+}
+.recurly .field.state input[type=text] {
+  width: 168px;
+}
+.recurly .field.state .error {
+  left: 300px;
+}
+.recurly .field.zip {
+  width: 100px;
+  margin-left: 20px;
+  margin-left: 20px;
+  clear: none;
+  clear: none;
+}
+.recurly .field.zip input[type=text] {
+  width: 88px;
+}
+.recurly .field.vat_number {
+  width: 140px;
+  display: none;
+}
+.recurly .field.vat_number input[type=text] {
+  width: 128px;
+}
+.recurly .field.vat_number.applicable {
+  display: block;
+}
+.recurly .only_zipstreet .zip.field,
+.recurly .only_zip .zip.field {
+  margin-left: 0;
+  clear: left;
+}
+.recurly .accepted_cards {
+  position: absolute;
+  top: 0px;
+  left: 140px;
+  width: 160px;
+}
+.recurly .card {
+  background-position: right top;
+  background-repeat: no-repeat;
+  text-indent: -3000px;
+  width: 40px;
+  height: 30px;
+  margin: 0;
+  padding: 0;
+  float: left;
+}
+.recurly .card.mastercard {
+  background-image: url("../images/mastercard.png");
+}
+.recurly .card.american_express {
+  background-image: url("../images/amex.png");
+}
+.recurly .card.visa {
+  background-image: url("../images/visa.png");
+}
+.recurly .card.discover {
+  background-image: url("../images/discover.png");
+}
+.recurly .card.no_match {
+  opacity: 0.3;
+}
+.recurly input[type=text],
+.recurly select {
+  vertical-align: middle;
+  color: #000;
+}
+.recurly input[type=text].invalid,
+.recurly select.invalid {
+  border-color: #a55;
+  background: #fee;
+  color: #311;
+}
+.recurly input[type=text] {
+  display: block;
+}
+.recurly input[type=text][disabled] {
+  background: #eee;
+}
+.recurly input[type=checkbox] {
+  color: #f00;
+}
+.recurly select {
+  color: inherit;
+  font-family: inherit;
+  width: 100%;
+}
+.recurly select > option {
+  color: inherit;
+}
+.recurly .due_now {
+  clear: both;
+  height: 70px;
+  line-height: 67px;
+  position: relative;
+}
+.recurly .due_now .title {
+  float: left;
+  font-size: 29px;
+  position: relative;
+}
+.recurly .due_now .cost {
+  float: right;
+  font-size: 33px;
+  font-weight: bold;
+  letter-spacing: 1px;
+  margin: 0;
+  position: relative;
+}
+.recurly .footer {
+  border-radius: 0px 0px 9px 9px;
+  margin: 0px;
+  padding: 20px;
+}
+.recurly.submitting .footer {
+  background: url("../images/submitting.gif") no-repeat 180px 28px;
+}
+.recurly button.submit {
+  height: 46px;
+  max-width: 600px;
+  font-size: 18px;
+  font-weight: 700;
+  color: #302106;
+  text-align: center;
+  margin-left: 0px;
+  border: 1px solid #767674;
+  background: #e7a500;
+  border-radius: 10px;
+  outline: none;
+  box-shadow: inset rgba(255,255,255,0.70) 0px 1px 0px, rgba(0,0,0,0.50) 0px 1px 3px;
+  background-image: -webkit-gradient(linear, 0% 20%, 0% 100%, from(#fecd00), to(#ce7b00));
+  background: -moz-linear-gradient(top, #fecd00, #ce7b00);
+  text-shadow: rgba(255,255,255,0.50) 0 1px 0;
+  padding: 10px 20px;
+}
+.recurly button.submit:hover {
+  color: #451;
+}
+.recurly button.submit:active {
+  top: 2px;
+  color: #302106;
+  text-shadow: rgba(255,255,255,0.57) 0 -1px 0;
+  outline: none;
+  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#ce7b00), to(#fecd00));
+  background: -moz-linear-gradient(top, #ce7b00, #fecd00);
+  box-shadow: rgba(255,255,255,0.69) 0px -1px 0px inset, rgba(0,0,0,0.26) 0px 2px 3px;
+}
+.recurly button.submit[disabled] {
+  position: relative;
+  height: 46px;
+  max-width: 600px;
+  padding: 0 10px;
+  font-weight: 700;
+  color: #555;
+  text-shadow: rgba(255,255,255,0.57) 0 1px 0;
+  text-align: center;
+  opacity: 0.75;
+  border: 1px solid #767674;
+  background: #e7a500;
+  -moz-border-radius: 10px;
+  -webkit-border-radius: 10px;
+  border-radius: 10px;
+  -webkit-user-select: none;
+  -moz-user-select: -moz-none;
+  outline: none;
+  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#dbd9d2), to(#999));
+  background: -moz-linear-gradient(top, #dbd9d2, #999);
+  -webkit-background-clip: padding-box;
+  -webkit-box-shadow: rgba(255,255,255,0.69) 0px 1px 0px inset, rgba(0,0,0,0.26) 0px 2px 3px;
+  box-shadow: rgba(255,255,255,0.70) 0px 1px 0px inset, rgba(0,0,0,0.27) 0px 2px 3px;
+}
+.iefail {
+  background: #666;
+  padding: 10px;
+  position: absolute;
+  top: -1%;
+  left: -1%;
+  height: 102%;
+  width: 102%;
+  z-index: 9999;
+}
+.iefail .chromeframe {
+  background: #fff;
+  border: 1px solid #ccc;
+  padding: 10px;
+}
+.iefail .chromeframe p {
+  text-align: center;
+}
+.iefail .chromeframe p.blast {
+  font-size: 1.3em;
+  font-weight: bold;
+}
+.iefail .chromeframe p a {
+  color: #4183c4;
+  text-transform: capitalize;
+}
diff --git a/modules/recurlyjs/images/amex.png b/modules/recurlyjs/images/amex.png
new file mode 100644
index 0000000..b38c687
--- /dev/null
+++ b/modules/recurlyjs/images/amex.png
Binary files differ
diff --git a/modules/recurlyjs/images/check.png b/modules/recurlyjs/images/check.png
new file mode 100755
index 0000000..98381d3
--- /dev/null
+++ b/modules/recurlyjs/images/check.png
Binary files differ
diff --git a/modules/recurlyjs/images/coupon_check.png b/modules/recurlyjs/images/coupon_check.png
new file mode 100755
index 0000000..16d1fa0
--- /dev/null
+++ b/modules/recurlyjs/images/coupon_check.png
Binary files differ
diff --git a/modules/recurlyjs/images/coupon_checking.gif b/modules/recurlyjs/images/coupon_checking.gif
new file mode 100755
index 0000000..e7e33a3
--- /dev/null
+++ b/modules/recurlyjs/images/coupon_checking.gif
Binary files differ
diff --git a/modules/recurlyjs/images/coupon_invalid.png b/modules/recurlyjs/images/coupon_invalid.png
new file mode 100755
index 0000000..ac71a30
--- /dev/null
+++ b/modules/recurlyjs/images/coupon_invalid.png
Binary files differ
diff --git a/modules/recurlyjs/images/coupon_valid.png b/modules/recurlyjs/images/coupon_valid.png
new file mode 100755
index 0000000..c99b87d
--- /dev/null
+++ b/modules/recurlyjs/images/coupon_valid.png
Binary files differ
diff --git a/modules/recurlyjs/images/discover.png b/modules/recurlyjs/images/discover.png
new file mode 100644
index 0000000..80f45c8
--- /dev/null
+++ b/modules/recurlyjs/images/discover.png
Binary files differ
diff --git a/modules/recurlyjs/images/due_now.png b/modules/recurlyjs/images/due_now.png
new file mode 100755
index 0000000..e04b45d
--- /dev/null
+++ b/modules/recurlyjs/images/due_now.png
Binary files differ
diff --git a/modules/recurlyjs/images/error.png b/modules/recurlyjs/images/error.png
new file mode 100755
index 0000000..6148add
--- /dev/null
+++ b/modules/recurlyjs/images/error.png
Binary files differ
diff --git a/modules/recurlyjs/images/loading.gif b/modules/recurlyjs/images/loading.gif
new file mode 100755
index 0000000..d42f72c
--- /dev/null
+++ b/modules/recurlyjs/images/loading.gif
Binary files differ
diff --git a/modules/recurlyjs/images/mastercard.png b/modules/recurlyjs/images/mastercard.png
new file mode 100644
index 0000000..b855840
--- /dev/null
+++ b/modules/recurlyjs/images/mastercard.png
Binary files differ
diff --git a/modules/recurlyjs/images/submitting.gif b/modules/recurlyjs/images/submitting.gif
new file mode 100755
index 0000000..d4a523b
--- /dev/null
+++ b/modules/recurlyjs/images/submitting.gif
Binary files differ
diff --git a/modules/recurlyjs/images/uncheck.png b/modules/recurlyjs/images/uncheck.png
new file mode 100755
index 0000000..a04300d
--- /dev/null
+++ b/modules/recurlyjs/images/uncheck.png
Binary files differ
diff --git a/modules/recurlyjs/images/visa.png b/modules/recurlyjs/images/visa.png
new file mode 100644
index 0000000..7db3b45
--- /dev/null
+++ b/modules/recurlyjs/images/visa.png
Binary files differ
diff --git a/modules/recurlyjs/includes/recurlyjs.pages.inc b/modules/recurlyjs/includes/recurlyjs.pages.inc
new file mode 100644
index 0000000..0263dc4
--- /dev/null
+++ b/modules/recurlyjs/includes/recurlyjs.pages.inc
@@ -0,0 +1,76 @@
+<?php
+/**
+ * @file
+ * Menu callbacks for the RecurlyJS module.
+ */
+
+/**
+ * Form callback; Modifies the Recurly form at admin/config/services/recurly.
+ */
+function _recurlyjs_form_recurly_settings_form_alter(&$form, &$form_state) {
+  $form['recurlyjs'] = array(
+    '#title' => t('RecurlyJS settings'),
+    '#type' => 'fieldset',
+    '#collapsible' => TRUE,
+  );
+  $form['recurlyjs']['recurlyjs_address_requirement'] = array(
+    '#title' => t('Address requirement level'),
+    '#type' => 'select',
+    '#options' => array(
+      'none' => t('None'),
+      'zip' => t('Zip code'),
+      'zipstreet' => t('Zip code and street'),
+      'full' => t('Full'),
+    ),
+    '#default_value' => variable_get('recurlyjs_address_requirement', 'none'),
+    '#description' => t('Choose the level of address information required. Collecting more address information reduces the probability of fraudulent accounts.'),
+  );
+}
+
+function recurlyjs_update_billing($form, $form_state, $entity_type, $entity) {
+  // Initialize the Recurly client with the site-wide settings.
+  if (!recurly_client_initialize()) {
+    $form['error'] = array(
+      '#markup' => t('Could not initialize the Recurly client.'),
+    );
+    return $form;
+  }
+
+  // See if we have a local mapping of entity ID to Recurly account code.
+  list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+  $account = recurly_account_load(array('entity_type' => $entity_type, 'entity_id' => $id));
+
+  try {
+    $billing_info = Recurly_BillingInfo::get($account->account_code);
+    $form['existing'] = array(
+      '#theme' => 'recurly_credit_card_information',
+      '#card_type' => check_plain($billing_info->card_type),
+      '#first_name' => check_plain($billing_info->first_name),
+      '#last_name' => check_plain($billing_info->last_name),
+      '#year' => check_plain($billing_info->year),
+      '#month' => check_plain($billing_info->month),
+      '#mask_length' => strcasecmp($billing_info->card_type, 'American Express') === 0 ? 11 : 12,
+      '#last_four' => check_plain($billing_info->last_four),
+    );
+  } catch (Recurly_NotFoundError $e) {
+    $form['existing'] = array(
+      '#markup' => t('None'),
+    );
+  }
+
+  $form['update'] = array(
+    '#type' => 'recurlyjs_billing',
+    '#account_code' => $account->account_code,
+    '#address_requirement' => variable_get('recurlyjs_address_requirement', 'none'),
+  );
+
+  $form['actions'] = array(
+    '#type' => 'actions',
+  );
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Update'),
+  );
+
+  return $form;
+}
diff --git a/modules/recurlyjs/recurly-element.js b/modules/recurlyjs/recurly-element.js
new file mode 100644
index 0000000..76e7461
--- /dev/null
+++ b/modules/recurlyjs/recurly-element.js
@@ -0,0 +1,35 @@
+/**
+ * @file
+ * FormAPI integration with the Recurly forms.
+ */
+(function ($) {
+
+Drupal.recurly = Drupal.recurly || {};
+Drupal.recurly.beforeInject = function(form) {
+  // Imitate typical Drupal element styling.
+  $(form).find('input[type=text]').addClass('form-text').parent().addClass('form-item');
+  $(form).find('select').addClass('form-select').parent().addClass('form-item');
+
+   // Remove the submit button. This will be triggered by the form submit.
+  $(form).find('.footer').remove();
+};
+
+Drupal.recurly.afterInject = function(form) {
+  // Nested form tags cause issues in IE, except if there are two of them.
+  // See http://anderwald.info/internet/nesting-form-tags-in-xhtml/
+  $(form).before('<form class="dummy-form" action="#" style="display: none"></form>');
+
+  $(form).parents('form:first').submit(function(e) {
+    if ($(this).find('input.recurly-token').val().length === 0) {
+      e.preventDefault();
+      $(form).triggerHandler('submit');
+    }
+  });
+};
+
+Drupal.recurly.successHandler = function(responseToken) {
+  $('.recurly-form-wrapper').find('input.recurly-token').val(responseToken);
+  $('.recurly-form-wrapper').parents('form:first').submit();
+};
+
+})(jQuery);
diff --git a/modules/recurlyjs/recurlyjs.info b/modules/recurlyjs/recurlyjs.info
new file mode 100644
index 0000000..e0d524c
--- /dev/null
+++ b/modules/recurlyjs/recurlyjs.info
@@ -0,0 +1,5 @@
+name = RecurlyJS
+description = Provides a Form API RecurlyJS field and integration with the Field and Entity systems.
+dependencies[] = recurly
+core = 7.x
+package = Recurly
diff --git a/modules/recurlyjs/recurlyjs.module b/modules/recurlyjs/recurlyjs.module
new file mode 100644
index 0000000..e369108
--- /dev/null
+++ b/modules/recurlyjs/recurlyjs.module
@@ -0,0 +1,348 @@
+<?php
+
+/**
+ * @file
+ * Uses RecurlyJS to provide a Form API field to subscribe to a service.
+ */
+
+/**
+ * Implements hook_menu().
+ */
+function recurlyjs_menu() {
+  $items['recurlyjs/test'] = array(
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('recurlyjs_test'),
+    'access callback' => TRUE,
+    'type' => MENU_CALLBACK,
+  );
+
+  // Configure built-in pages if enabled.
+  $entity_type = variable_get('recurly_entity_type', 'user');
+  if ($entity_type) {
+    $entity_info = entity_get_info();
+    $bundle = variable_get('recurly_bundle_' . $entity_type, NULL);
+    $bundle = empty($bundle) ? $entity_type : $bundle;
+
+    $entity_path = $entity_type . '/%' . $entity_type;
+    $items[$entity_path . '/subscription/billing'] = array(
+      'title' => 'Update billing information',
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('recurlyjs_update_billing', $entity_type, 1),
+      'access callback' => $entity_info[$entity_type]['access callback'],
+      'access arguments' => array('update', 1, NULL, $entity_type),
+      'type' => MENU_LOCAL_TASK,
+      'weight' => 2,
+      'file' => 'includes/recurlyjs.pages.inc',
+    );
+  }
+
+  return $items;
+}
+
+/**
+ * Test page.
+ */
+function recurlyjs_test($form, &$form_state) {
+  $form['payment'] = array(
+    '#type' => 'recurlyjs_subscribe',
+    '#plan_code' => 'sg1-plus',
+    '#address_requirement' => variable_get('recurlyjs_address_requirement', 'none'),
+  );
+
+  $form['actions']['#type'] = 'actions';
+  $form['actions']['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Submit'),
+  );
+  return $form;
+}
+
+/**
+ * Implements hook_element_info().
+ */
+function recurlyjs_element_info() {
+  $common = array(
+    '#input' => TRUE,
+    '#recurly_css' => drupal_get_path('module', 'recurlyjs') . '/css/recurly-widget.css',
+
+    // Global options:
+    // See http://docs.recurly.com/api/recurlyjs/reference#js-config
+    '#country' => 'US',
+    '#currency' => 'USD',
+    '#subdomain' => variable_get('recurly_subdomain', ''),
+    '#vatpercent' => NULL,
+    '#locale' => NULL,
+    '#terms_url' => NULL,
+    '#privacy_url' => NULL,
+
+    // Account options (not displayed to user):
+    // See http://docs.recurly.com/api/recurlyjs/reference#account-object
+    '#account_code' => NULL,
+    '#account_username' => NULL,
+    '#account_email' => NULL,
+    '#account_first_name' => NULL,
+    '#account_last_name' => NULL,
+    '#account_company_name' => NULL,
+
+    // Billing information; prepopulates options:
+    // See http://docs.recurly.com/api/recurlyjs/reference#billing-info-object
+    '#billing_first_name' => NULL,
+    '#billing_last_name' => NULL,
+    '#billing_address1' => NULL,
+    '#billing_address2' => NULL,
+    '#billing_city' => NULL,
+    '#billing_state' => NULL,
+    '#billing_country' => NULL,
+    '#billing_zip' => NULL,
+    '#billing_phone' => NULL,
+    '#billing_vat_number' => NULL,
+
+    // Common options between all forms.
+    '#address_requirement' => 'full',
+    '#distinguish_contact_from_billing_info' => NULL,
+  );
+
+  // Element to start a subscription.
+  $types['recurlyjs_subscribe'] = array(
+    '#process' => array('recurlyjs_element_subscription_expand', 'recurlyjs_element_expand'),
+    '#recurly_operation' => 'buildSubscriptionForm',
+    '#theme' => array('recurlyjs__subscribe', 'recurlyjs'),
+
+    // See http://docs.recurly.com/api/recurlyjs/reference#subscription-object.
+    '#plan_code' => NULL,
+    '#trial_ends_at' => NULL, // ISO 8601 formatted.
+    '#starts_at' => NULL, // ISO 8601 formatted.
+    '#total_billing_cycles' => NULL, // Defaults to infinite.
+    '#first_renewal_date' => NULL, // Defaults one cycle from today.
+    '#coupon_code' => NULL,
+    '#enable_add_ons' => TRUE,
+    '#enable_coupons' => TRUE,
+    '#collect_phone' => FALSE,
+    '#collect_company' => FALSE,
+  ) + $common;
+
+  // Element to update billing information for an existing subscription.
+  $types['recurlyjs_billing'] = array(
+    '#process' => array('recurlyjs_element_expand'),
+    '#account_code' => NULL,
+    '#recurly_operation' => 'buildBillingInfoUpdateForm',
+    '#theme' => array('recurlyjs__billing', 'recurlyjs'),
+  ) + $common;
+
+  // Element to do a one-time payment.
+  $types['recurlyjs_transaction'] = array(
+    '#process' => array('recurlyjs_element_expand'),
+    '#recurly_operation' => 'buildTransactionForm',
+    '#theme' => array('recurlyjs__transaction', 'recurlyjs'),
+
+    // See http://docs.recurly.com/api/recurlyjs/reference#transaction-object
+    '#amount' => NULL, // Specified in cents, i.e. 1023 = $10.23.
+    '#description' => NULL,
+    '#accounting_code' => NULL,
+    '#coupon_code' => NULL,
+    '#enable_add_ons' => TRUE,
+    '#enable_coupons' => TRUE,
+    '#collect_contact_info' => TRUE,
+    '#collect_phone' => FALSE,
+    '#collect_company' => FALSE,
+  ) + $common;
+  return $types;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function recurlyjs_theme() {
+  $items['recurlyjs'] = array(
+    'render element' => 'element',
+    'pattern' => 'recurlyjs__',
+    'template' => 'templates/recurlyjs',
+  );
+  $items['recurlyjs_update_billing'] = array(
+    'render element' => 'form',
+    'template' => 'templates/recurlyjs-update-billing',
+  );
+  return $items;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function recurlyjs_form_recurly_settings_form_alter(&$form, &$form_state) {
+  module_load_include('inc', 'recurlyjs', 'includes/recurlyjs.pages');
+  _recurlyjs_form_recurly_settings_form_alter($form, $form_state);
+}
+
+/**
+ * FormAPI #process callback for the recurlyjs_subscription element type.
+ */
+function recurlyjs_element_subscription_expand($element) {
+  recurly_client_initialize();
+
+  // Keys within the subscription object.
+  // http://docs.recurly.com/api/recurlyjs/reference#subscription-object
+  $subscription_keys = array(
+    'plan_code' => NULL,
+    'trial_ends_at' => NULL,
+    'starts_at' => NULL,
+    'total_billing_cylces' => NULL,
+    'first_renewal_date' => NULL,
+  );
+  $options['subscription'] = array();
+  foreach ($subscription_keys as $form_key => $jsonKey) {
+    if (isset($element['#' . $form_key])) {
+      $options['subscription'][$form_key] = $element['#' . $form_key];
+    }
+  }
+
+  $element['#recurly_options'] = $options;
+
+  return $element;
+}
+
+/**
+ * FormAPI #process callback for the recurlyjs element type.
+ */
+function recurlyjs_element_expand($element) {
+  recurly_client_initialize();
+
+  // Some keys are part of the global configuration of Recurly, while others are
+  // form-specific configuration options. Build together the global options for
+  // Recurly first here.
+  $global_keys = array(
+    'subdomain' => NULL,
+    'currency' => NULL,
+    'country' => NULL,
+    'vatpercent' => 'VATPercent',
+    'locale' => NULL,
+    'terms_url' => 'termsOfServiceURL',
+    'privacy_url' => 'privacyPolicyURL',
+  );
+  $global_config = array();
+  foreach ($global_keys as $form_key => $jsonKey) {
+    if (isset($element['#' . $form_key])) {
+      if (isset($jsonKey)) {
+        $global_config[$jsonKey] = $element['#' . $form_key];
+      }
+      else {
+        $global_config[$form_key] = $element['#' . $form_key];
+      }
+    }
+  }
+
+  // Merge together individual field options with common options shared between
+  // multiple field types. Note that not all options listed below apply to all
+  // field types, but they have no negative effect if added.
+  $options = isset($element['#recurly_options']) ? $element['#recurly_options'] : array();
+  $option_keys = array(
+    'plan_code' => 'planCode',
+    'account_code' => 'accountCode',
+    'accepted_cards' => 'acceptedCards',
+    'collect_phone' => 'collectPhone',
+    'collect_company' => 'collectCompany',
+    'enable_add_ons' => 'enableAddOns',
+    'enable_coupons' => 'enableCoupons',
+    'address_requirement' => 'addressRequirement',
+    'separate_contact' => 'distinguishContactFromBillingInfo',
+    'terms_url' => 'termsOfServiceURL',
+    'privacy_url' => 'privacyPolicyURL',
+    'collect_contact_info' => 'collectContactInfo',
+    'collect_phone' => 'collectPhone',
+    'collect_company' => 'collectCompany',
+  );
+  foreach ($option_keys as $form_key => $jsonKey) {
+    if (isset($element['#' . $form_key])) {
+      $options[$jsonKey] = $element['#' . $form_key];
+    }
+  }
+
+  // Convert options which have #account_ or #billing_ prefixes.
+  $convert_keys = array(
+    'first_name' => 'firstName',
+    'last_name' => 'lastName',
+    'email' => 'email',
+    'company_name' => 'companyName',
+    'vat_number' => 'vatNumber',
+    'coupon_code' => 'couponCode',
+  );
+  foreach (element_properties($element) as $property) {
+    if (strpos($property, '#account_') === 0 && $property !== '#account_code') {
+      $property_name = substr($property, 9);
+      $json_name = isset($convert_key[$property_name]) ? $convert_key[$property_name] : $property_name;
+      $options['account'][$json_name] = $element[$property];
+    }
+    elseif (strpos($property, '#billing_') === 0) {
+      $property_name = substr($property, 9);
+      $json_name = isset($convert_key[$property_name]) ? $convert_key[$property_name] : $property_name;
+      $options['billingInfo'][$json_name] = $element[$property];
+    }
+  }
+
+  // Settings that must exist and cannot be changed.
+  $options['target'] = '#' . $element['#id'];
+  $options['signature'] = Recurly_js::sign($options);
+  $global_config['beforeInject'] = 'Drupal.recurly.beforeInject';
+  $global_config['afterInject'] = 'Drupal.recurly.afterInject';
+  $global_config['successHandler'] = 'Drupal.recurly.successHandler';
+
+  // Shorten up and prepare variables for inline JavaScript code.
+  $global_config = json_encode($global_config);
+  $field_config = json_encode($options);
+  $recurly_operation = $element['#recurly_operation'];
+
+  // Change the beforeInject option to be an actual function rather than string.
+  $global_config = str_replace('"Drupal.recurly.beforeInject"', 'Drupal.recurly.beforeInject', $global_config);
+  $global_config = str_replace('"Drupal.recurly.afterInject"', 'Drupal.recurly.afterInject', $global_config);
+  $global_config = str_replace('"Drupal.recurly.successHandler"', 'Drupal.recurly.successHandler', $global_config);
+
+  // Add RecurlyJS and the inline code to the page.
+  $element['#attached']['js'][] = recurlyjs_path() . '/build/recurly.js';
+  $element['#attached']['js'][] = drupal_get_path('module', 'recurlyjs') . '/recurly-element.js';
+  $element['#attached']['js'][] = array(
+    'type' => 'inline',
+    'data' => "Recurly.config($global_config); Recurly.$recurly_operation($field_config);",
+  );
+  // Convenience CSS file location option.
+  if (isset($element['#recurly_css'])) {
+    $element['#attached']['css'][] = $element['#recurly_css'];
+  }
+
+  // The token field is populated via JavaScript then confirmed on submission.
+  $element['recurly_token'] = array(
+    '#type' => 'hidden',
+    '#default_value' => '',
+    '#element_validate' => array('recurly_element_token_validate'),
+    '#attributes' => array('class' => array('recurly-token'))
+  );
+
+  return $element;
+}
+
+/**
+ * FormAPI #element_validate handler for Recurly elements.
+ */
+function recurly_element_token_validate($element, &$form_state) {
+  recurly_client_initialize();
+  try {
+    $form_state['recurly_result'] = Recurly_js::fetch($element['#value']);
+  }
+  catch (Recurly_NotFoundError $e) {
+    form_error($element, t('The information could not be processed because the payment gateway returned the following error: @error', array('@error' => $e->getMessage())));
+  }
+}
+
+/**
+ * Retrieve the path to the RecurlyJS library file.
+ */
+function recurlyjs_path() {
+  // If we can find a path in the libraries directory to the Recurly PHP client
+  // library...
+  if (($path = libraries_get_path('recurly-js')) && file_exists($path . '/build/recurly.js')) {
+    // Include the library files and configure authentication.
+    return $path;
+  }
+  else {
+    watchdog('recurly', 'Could not find the Recurly JS client library in sites/all/libraries/recurly-js.', array(), WATCHDOG_ERROR);
+    return FALSE;
+  }
+}
diff --git a/modules/recurlyjs/templates/recurlyjs-update-billing.tpl.php b/modules/recurlyjs/templates/recurlyjs-update-billing.tpl.php
new file mode 100644
index 0000000..44f0196
--- /dev/null
+++ b/modules/recurlyjs/templates/recurlyjs-update-billing.tpl.php
@@ -0,0 +1,16 @@
+<?php
+/**
+ * @file
+ * Displays the form for updating a credit card number for a subscription.
+ */
+?>
+
+<?php if (isset($form['existing']['#last_four'])): ?>
+  <p><?php print t('We currently have the following credit card on file:'); ?></p>
+  <p><?php print drupal_render($form['existing']); ?></p>
+  <p><?php print t('To change your credit card, update your information below:'); ?></p>
+<?php else: ?>
+  <p><?php print t('No credit card currently on file.'); ?></p>
+<?php endif; ?>
+
+<?php print drupal_render_children($form); ?>
diff --git a/modules/recurlyjs/templates/recurlyjs.tpl.php b/modules/recurlyjs/templates/recurlyjs.tpl.php
new file mode 100644
index 0000000..427d993
--- /dev/null
+++ b/modules/recurlyjs/templates/recurlyjs.tpl.php
@@ -0,0 +1,17 @@
+<?php
+/**
+ * @file
+ * Prints the RecurlyJS form elements. This template is responsible for all
+ * three different form types:
+ *  - recurlyjs_subscribe
+ *  - recurlyjs_billing
+ *  - recurlyjs_transaction
+ */
+?>
+<div class="recurly-form-wrapper">
+  <div id="<?php print $element['#id']; ?>">
+    <script type="text/javascript">document.write('<?php print t('Please wait while loading...') ?>');</script>
+    <noscript><?php print t('This order form requires JavaScript and Cookies to be enabled in your browser.'); ?></noscript>
+  </div>
+  <?php print drupal_render_children($element); ?>
+</div>