[gnome-software/wip/rancell/paid: 1/2] Add support for app purchasing



commit b829e3e59013e1e5f8c0aef9646961e0b3fefe1c
Author: Robert Ancell <robert ancell canonical com>
Date:   Tue Feb 23 14:55:05 2016 +1300

    Add support for app purchasing

 configure.ac                     |    2 +-
 po/POTFILES.in                   |    3 +
 src/Makefile.am                  |    8 ++
 src/gnome-software.gresource.xml |    1 +
 src/gs-app-row.c                 |   23 +++++
 src/gs-app.c                     |   47 +++++++++
 src/gs-app.h                     |    4 +
 src/gs-page.c                    |   98 ++++++++++++++++++
 src/gs-page.h                    |    3 +
 src/gs-payment-method.c          |  144 +++++++++++++++++++++++++++
 src/gs-payment-method.h          |   49 +++++++++
 src/gs-plugin-loader.c           |  203 ++++++++++++++++++++++++++++++++++++++
 src/gs-plugin-loader.h           |   13 +++
 src/gs-plugin-vfuncs.h           |   42 ++++++++
 src/gs-plugin.h                  |    1 +
 src/gs-price.c                   |  174 ++++++++++++++++++++++++++++++++
 src/gs-price.h                   |   50 +++++++++
 src/gs-purchase-dialog.c         |   58 +++++++++++
 src/gs-purchase-dialog.h         |   39 +++++++
 src/gs-purchase-dialog.ui        |   80 +++++++++++++++
 src/gs-shell-details.c           |   38 +++++++
 src/gs-shell-search.c            |    5 +-
 src/gs-utils.h                   |    1 +
 src/plugins/gs-plugin-dummy.c    |   74 ++++++++++++++-
 24 files changed, 1156 insertions(+), 4 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index af9b547..7615977 100644
--- a/configure.ac
+++ b/configure.ac
@@ -61,7 +61,7 @@ dnl ---------------------------------------------------------------------------
 dnl - Check library dependencies
 dnl ---------------------------------------------------------------------------
 PKG_CHECK_MODULES(GTK, gtk+-3.0 >= 3.18.2 gio-unix-2.0 gtkspell3-3.0)
-PKG_CHECK_MODULES(APPSTREAM, appstream-glib >= 0.5.15)
+PKG_CHECK_MODULES(APPSTREAM, appstream-glib >= 0.5.17)
 PKG_CHECK_MODULES(GDK_PIXBUF, gdk-pixbuf-2.0 >= 2.31.5)
 PKG_CHECK_MODULES(JSON_GLIB, json-glib-1.0 >= 1.1.1)
 PKG_CHECK_MODULES(SQLITE, sqlite3)
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 123211e..4f9b5da 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -25,6 +25,9 @@ src/gs-main.c
 src/gs-page.c
 src/gs-plugin-loader.c
 src/gs-popular-tile.c
+src/gs-price.c
+src/gs-purchase-dialog.c
+[type: gettext/glade]src/gs-purchase-dialog.ui
 src/gs-removal-dialog.c
 [type: gettext/glade]src/gs-removal-dialog.ui
 src/gs-review-dialog.c
diff --git a/src/Makefile.am b/src/Makefile.am
index ae71985..8fb00f1 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -59,6 +59,7 @@ UI_FILES =                                            \
        gs-info-bar.ui                                  \
        gs-menus.ui                                     \
        gs-popular-tile.ui                              \
+       gs-purchase-dialog.ui                           \
        gs-removal-dialog.ui                            \
        gs-review-dialog.ui                             \
        gs-review-histogram.ui                          \
@@ -110,6 +111,7 @@ gnome_software_cmd_SOURCES =                                \
        gs-app.c                                        \
        gs-app-list.c                                   \
        gs-auth.c                                       \
+       gs-price.c                                      \
        gs-review.c                                     \
        gs-cmd.c                                        \
        gs-common.c                                     \
@@ -190,12 +192,18 @@ gnome_software_SOURCES =                          \
        gs-os-release.h                                 \
        gs-page.c                                       \
        gs-page.h                                       \
+       gs-payment-method.c                             \
+       gs-payment-method.h                             \
        gs-plugin.c                                     \
        gs-plugin.h                                     \
        gs-plugin-private.h                             \
        gs-plugin-vfuncs.h                              \
+       gs-price.c                                      \
+       gs-price.h                                      \
        gs-progress-button.c                            \
        gs-progress-button.h                            \
+       gs-purchase-dialog.c                            \
+       gs-purchase-dialog.h                            \
        gs-removal-dialog.c                             \
        gs-removal-dialog.h                             \
        gs-review.c                                     \
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index e6b4e79..7933e2c 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -14,6 +14,7 @@
   <file preprocess="xml-stripblanks">gs-history-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-info-bar.ui</file>
   <file preprocess="xml-stripblanks">gs-popular-tile.ui</file>
+  <file preprocess="xml-stripblanks">gs-purchase-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-removal-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-review-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-review-histogram.ui</file>
diff --git a/src/gs-app-row.c b/src/gs-app-row.c
index 8459bef..9595c9a 100644
--- a/src/gs-app-row.c
+++ b/src/gs-app-row.c
@@ -30,6 +30,7 @@
 #include "gs-progress-button.h"
 #include "gs-common.h"
 #include "gs-folders.h"
+#include "gs-price.h"
 
 typedef struct
 {
@@ -155,6 +156,7 @@ gs_app_row_refresh_button (GsAppRow *app_row, gboolean missing_search_result)
 {
        GsAppRowPrivate *priv = gs_app_row_get_instance_private (app_row);
        GtkStyleContext *context;
+       GPtrArray *prices;
 
        /* disabled */
        if (!priv->show_buttons) {
@@ -204,6 +206,27 @@ gs_app_row_refresh_button (GsAppRow *app_row, gboolean missing_search_result)
                        gtk_button_set_label (GTK_BUTTON (priv->button), _("Remove"));
                }
                break;
+       case AS_APP_STATE_PURCHASABLE:
+               gtk_widget_set_visible (priv->button, TRUE);
+               prices = gs_app_get_prices (priv->app);
+               if (prices->len > 0) {
+                       GsPrice *price = g_ptr_array_index (prices, 0);
+                       g_autofree gchar *text = NULL;
+                       text = gs_price_to_string (price);
+                       gtk_button_set_label (GTK_BUTTON (priv->button), text);
+               } else {
+                       /* TRANSLATORS: this is a button next to the search results that
+                        * allows the application to be easily purchased */
+                       gtk_button_set_label (GTK_BUTTON (priv->button), _("Buy"));
+               }
+               break;
+       case AS_APP_STATE_PURCHASING:
+               gtk_widget_set_visible (priv->button, TRUE);
+               gtk_widget_set_sensitive (priv->button, FALSE);
+               /* TRANSLATORS: this is a button next to the search results that
+                * allows the status of an application being purchased */
+               gtk_button_set_label (GTK_BUTTON (priv->button), _("Buying"));
+               break;
        case AS_APP_STATE_UPDATABLE:
        case AS_APP_STATE_INSTALLED:
                if (!gs_app_has_quirk (priv->app, AS_APP_QUIRK_COMPULSORY))
diff --git a/src/gs-app.c b/src/gs-app.c
index 2f750ba..e509845 100644
--- a/src/gs-app.c
+++ b/src/gs-app.c
@@ -53,6 +53,7 @@
 #include "gs-app-private.h"
 #include "gs-plugin.h"
 #include "gs-utils.h"
+#include "gs-price.h"
 
 struct _GsApp
 {
@@ -91,6 +92,7 @@ struct _GsApp
        gchar                   *management_plugin;
        guint                    match_value;
        guint                    priority;
+       GPtrArray               *prices; /* of GsPrice */
        gint                     rating;
        GArray                  *review_ratings;
        GPtrArray               *reviews; /* of GsReview */
@@ -381,6 +383,13 @@ gs_app_to_string (GsApp *app)
                gs_app_kv_lpad (str, "origin-ui", app->origin_ui);
        if (app->origin_hostname != NULL && app->origin_hostname[0] != '\0')
                gs_app_kv_lpad (str, "origin-hostname", app->origin_hostname);
+       for (i = 0; i < app->prices->len; i++) {
+               GsPrice *price = g_ptr_array_index (app->prices, i);
+               g_autofree gchar *key = NULL, *text = NULL;
+               key = g_strdup_printf ("price-%02i", i);
+               text = gs_price_to_string (price);
+               gs_app_kv_lpad (str, key, text);
+       }
        if (app->rating != -1)
                gs_app_kv_printf (str, "rating", "%i", app->rating);
        if (app->review_ratings != NULL) {
@@ -599,6 +608,7 @@ gs_app_set_state_internal (GsApp *app, AsAppState state)
                /* unknown has to go into one of the stable states */
                if (state == AS_APP_STATE_INSTALLED ||
                    state == AS_APP_STATE_QUEUED_FOR_INSTALL ||
+                   state == AS_APP_STATE_PURCHASABLE ||
                    state == AS_APP_STATE_AVAILABLE ||
                    state == AS_APP_STATE_AVAILABLE_LOCAL ||
                    state == AS_APP_STATE_UPDATABLE ||
@@ -618,6 +628,21 @@ gs_app_set_state_internal (GsApp *app, AsAppState state)
                    state == AS_APP_STATE_AVAILABLE)
                        state_change_ok = TRUE;
                break;
+       case AS_APP_STATE_PURCHASABLE:
+               /* purchasable has to go into an available state */
+               if (state == AS_APP_STATE_UNKNOWN ||
+                   state == AS_APP_STATE_AVAILABLE)
+                       state_change_ok = TRUE;
+               break;
+       case AS_APP_STATE_PURCHASING:
+               /* purchasing has to go into an stable state */
+               if (state == AS_APP_STATE_UNKNOWN ||
+                   state == AS_APP_STATE_INSTALLED ||
+                   state == AS_APP_STATE_UPDATABLE ||
+                   state == AS_APP_STATE_UPDATABLE_LIVE ||
+                   state == AS_APP_STATE_AVAILABLE)
+                       state_change_ok = TRUE;
+               break;
        case AS_APP_STATE_AVAILABLE:
                /* available has to go into an action state */
                if (state == AS_APP_STATE_UNKNOWN ||
@@ -1908,6 +1933,26 @@ gs_app_set_management_plugin (GsApp *app, const gchar *management_plugin)
 }
 
 /**
+ * gs_app_get_prices:
+ */
+GPtrArray *
+gs_app_get_prices (GsApp *app)
+{
+       g_return_val_if_fail (GS_IS_APP (app), NULL);
+       return app->prices;
+}
+
+/**
+ * gs_app_add_price:
+ */
+void
+gs_app_add_price (GsApp *app, gdouble amount, const gchar *currency)
+{
+       g_return_if_fail (GS_IS_APP (app));
+       g_ptr_array_add (app->prices, gs_price_new (amount, currency));
+}
+
+/**
  * gs_app_get_rating:
  * @app: a #GsApp
  *
@@ -2832,6 +2877,7 @@ gs_app_dispose (GObject *object)
        g_clear_pointer (&app->history, g_ptr_array_unref);
        g_clear_pointer (&app->related, g_ptr_array_unref);
        g_clear_pointer (&app->screenshots, g_ptr_array_unref);
+       g_clear_pointer (&app->prices, g_ptr_array_unref);
        g_clear_pointer (&app->reviews, g_ptr_array_unref);
        g_clear_pointer (&app->icons, g_ptr_array_unref);
 
@@ -2991,6 +3037,7 @@ gs_app_init (GsApp *app)
        app->related = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->history = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->screenshots = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+       app->prices = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->icons = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->metadata = g_hash_table_new_full (g_str_hash,
diff --git a/src/gs-app.h b/src/gs-app.h
index e66108e..7422419 100644
--- a/src/gs-app.h
+++ b/src/gs-app.h
@@ -206,6 +206,10 @@ const gchar        *gs_app_get_metadata_item       (GsApp          *app,
 void            gs_app_set_metadata            (GsApp          *app,
                                                 const gchar    *key,
                                                 const gchar    *value);
+GPtrArray      *gs_app_get_prices              (GsApp          *app);
+void            gs_app_add_price               (GsApp          *app,
+                                                gdouble         amount,
+                                                const gchar    *currency);
 gint            gs_app_get_rating              (GsApp          *app);
 void            gs_app_set_rating              (GsApp          *app,
                                                 gint            rating);
diff --git a/src/gs-page.c b/src/gs-page.c
index 3e6cb08..9efac30 100644
--- a/src/gs-page.c
+++ b/src/gs-page.c
@@ -29,6 +29,7 @@
 #include "gs-shell.h"
 #include "gs-common.h"
 #include "gs-auth-dialog.h"
+#include "gs-price.h"
 
 typedef struct
 {
@@ -61,6 +62,33 @@ gs_page_helper_free (GsPageHelper *helper)
 G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPageHelper, gs_page_helper_free);
 
 static void
+gs_page_app_purchased_cb (GObject *source,
+                          GAsyncResult *res,
+                          gpointer user_data)
+{
+       g_autoptr(GsPageHelper) helper = (GsPageHelper *) user_data;
+       GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+       GsPage *page = helper->page;
+       GsPagePrivate *priv = gs_page_get_instance_private (page);
+       gboolean ret;
+       g_autoptr(GError) error = NULL;
+
+       ret = gs_plugin_loader_app_action_finish (plugin_loader,
+                                                 res,
+                                                 &error);
+       if (!ret) {
+               g_warning ("failed to purchase %s: %s",
+                          gs_app_get_id (helper->app),
+                          error->message);
+               //gs_app_notify_failed_modal (helper->app,
+               //                            gs_shell_get_window (priv->shell),
+               //                            GS_PLUGIN_LOADER_ACTION_PURCHASE,
+               //                            error);
+               return;
+       }
+}
+
+static void
 gs_page_app_installed_cb (GObject *source,
                           GAsyncResult *res,
                           gpointer user_data);
@@ -291,6 +319,76 @@ gs_page_set_header_end_widget (GsPage *page, GtkWidget *widget)
        g_set_object (&priv->header_end_widget, widget);
 }
 
+static void
+gs_page_purchase_app_response_cb (GtkDialog *dialog,
+                                 gint response,
+                                 GsPageHelper *helper)
+{
+       GsPagePrivate *priv = gs_page_get_instance_private (helper->page);
+       GPtrArray *prices;
+
+       /* not agreed */
+       if (response != GTK_RESPONSE_OK) {
+               gs_page_helper_free (helper);
+               return;
+       }
+       g_debug ("purchase %s", gs_app_get_id (helper->app));
+       prices = gs_app_get_prices (helper->app);
+       gs_plugin_loader_app_purchase_async (priv->plugin_loader,
+                                            helper->app,
+                                            g_ptr_array_index (prices, 0), // FIXME: User should pick price, 
check if no prices
+                                            helper->cancellable,
+                                            gs_page_app_purchased_cb,
+                                            helper);
+}
+
+void
+gs_page_purchase_app (GsPage *page, GsApp *app, GCancellable *cancellable)
+{
+       GsPagePrivate *priv = gs_page_get_instance_private (page);
+       GsPageHelper *helper;
+       GtkWidget *dialog;
+       GPtrArray *prices;
+       g_autofree gchar *price_text = NULL, *escaped = NULL;
+
+       helper = g_slice_new0 (GsPageHelper);
+       helper->app = g_object_ref (app);
+       helper->page = g_object_ref (page);
+       helper->cancellable = g_object_ref (cancellable);
+
+       /* ask for confirmation */
+       dialog = gtk_message_dialog_new (gs_shell_get_window (priv->shell),
+                                        GTK_DIALOG_MODAL,
+                                        GTK_MESSAGE_QUESTION,
+                                        GTK_BUTTONS_CANCEL,
+                                        /* TRANSLATORS: this is a prompt message, and
+                                         * '%s' is an application summary, e.g. 'GNOME Clocks' */
+                                        _("Are you sure you want to purchase %s?"),
+                                        gs_app_get_name (app));
+       prices = gs_app_get_prices (app);
+       if (prices->len > 0) {
+               GsPrice *price = g_ptr_array_index (prices, 0); // FIXME: Give option of price to choose
+               g_autofree gchar *text = NULL;
+               price_text = gs_price_to_string (price);
+       } else {
+               price_text = g_strdup ("nothing"); // FIXME
+       }
+       escaped = g_markup_escape_text (gs_app_get_name (app), -1);
+       gtk_message_dialog_format_secondary_markup (GTK_MESSAGE_DIALOG (dialog),
+                                                   /* TRANSLATORS: Describing what will be purchased */
+                                                    _("You will be charged %s and %s will be installed."),
+                                                    price_text,
+                                                    escaped);
+
+       /* TRANSLATORS: this is button text to purchase the application */
+       gtk_dialog_add_button (GTK_DIALOG (dialog), _("Purchase"), GTK_RESPONSE_OK);
+
+       /* handle this async */
+       g_signal_connect (dialog, "response",
+                         G_CALLBACK (gs_page_purchase_app_response_cb), helper);
+       gs_shell_modal_dialog_present (priv->shell, GTK_DIALOG (dialog));
+}
+
 void
 gs_page_install_app (GsPage *page, GsApp *app, GCancellable *cancellable)
 {
diff --git a/src/gs-page.h b/src/gs-page.h
index 1f6b59c..050b198 100644
--- a/src/gs-page.h
+++ b/src/gs-page.h
@@ -54,6 +54,9 @@ void           gs_page_set_header_start_widget        (GsPage         *page,
 GtkWidget      *gs_page_get_header_end_widget          (GsPage         *page);
 void            gs_page_set_header_end_widget          (GsPage         *page,
                                                         GtkWidget      *widget);
+void            gs_page_purchase_app                   (GsPage         *page,
+                                                        GsApp          *app,
+                                                        GCancellable   *cancellable);
 void            gs_page_install_app                    (GsPage         *page,
                                                         GsApp          *app,
                                                         GCancellable   *cancellable);
diff --git a/src/gs-payment-method.c b/src/gs-payment-method.c
new file mode 100644
index 0000000..92b7a38
--- /dev/null
+++ b/src/gs-payment-method.c
@@ -0,0 +1,144 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gs-payment-method.h"
+
+struct _GsPaymentMethod
+{
+       GObject                  parent_instance;
+
+       gchar                   *description;
+       GHashTable              *metadata;
+};
+
+G_DEFINE_TYPE (GsPaymentMethod, gs_payment_method, G_TYPE_OBJECT)
+
+/**
+ * gs_payment_method_get_description:
+ * @method: a #GsPaymentMethod
+ *
+ * Gets the payment method description.
+ *
+ * Returns: a one line description for this payment method, e.g. "**** **** **** 1111 (exp 23/2020)"
+ */
+const gchar *
+gs_payment_method_get_description (GsPaymentMethod *method)
+{
+       g_return_val_if_fail (GS_IS_PAYMENT_METHOD (method), NULL);
+       return method->description;
+}
+
+/**
+ * gs_payment_method_set_description:
+ * @method: a #GsPaymentMethod
+ * @description: Human readable description for this payment method, e.g. "**** **** **** 1111 (exp 23/2020)"
+ *
+ * Sets the one line description that may be displayed for this payment method.
+ */
+void
+gs_payment_method_set_description (GsPaymentMethod *method, const gchar *description)
+{
+       g_return_if_fail (GS_IS_PAYMENT_METHOD (method));
+       g_free (method->description);
+       method->description = g_strdup (description);
+}
+
+/**
+ * gs_payment_method_get_metadata_item:
+ * @method: a #GsPaymentMethod
+ * @key: a string
+ *
+ * Gets some metadata from a payment method object.
+ * It is left for the the plugin to use this method as required, but a
+ * typical use would be to store an ID for this payment, or payment information.
+ *
+ * Returns: A string value, or %NULL for not found
+ */
+const gchar *
+gs_payment_method_get_metadata_item (GsPaymentMethod *method, const gchar *key)
+{
+       g_return_val_if_fail (GS_IS_PAYMENT_METHOD (method), NULL);
+       g_return_val_if_fail (key != NULL, NULL);
+       return g_hash_table_lookup (method->metadata, key);
+}
+
+/**
+ * gs_payment_method_add_metadata:
+ * @method: a #GsPaymentMethod
+ * @key: a string
+ * @value: a string
+ *
+ * Adds metadata to the review object.
+ * It is left for the the plugin to use this method as required, but a
+ * typical use would be to store an ID for this payment, or payment information.
+ */
+void
+gs_payment_method_add_metadata (GsPaymentMethod *method, const gchar *key, const gchar *value)
+{
+       g_return_if_fail (GS_IS_PAYMENT_METHOD (method));
+       g_hash_table_insert (method->metadata, g_strdup (key), g_strdup (value));
+}
+
+static void
+gs_payment_method_finalize (GObject *object)
+{
+       GsPaymentMethod *method = GS_PAYMENT_METHOD (object);
+
+       g_free (method->description);
+       g_hash_table_unref (method->metadata);
+
+       G_OBJECT_CLASS (gs_payment_method_parent_class)->finalize (object);
+}
+
+static void
+gs_payment_method_class_init (GsPaymentMethodClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = gs_payment_method_finalize;
+}
+
+static void
+gs_payment_method_init (GsPaymentMethod *method)
+{
+       method->metadata = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                 g_free, g_free);
+}
+
+/**
+ * gs_payment_method_new:
+ *
+ * Creates a new payment method object.
+ *
+ * Return value: a new #GsPaymentMethod object.
+ **/
+GsPaymentMethod *
+gs_payment_method_new (void)
+{
+       GsPaymentMethod *method;
+       method = g_object_new (GS_TYPE_PAYMENT_METHOD, NULL);
+       return GS_PAYMENT_METHOD (method);
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-payment-method.h b/src/gs-payment-method.h
new file mode 100644
index 0000000..d1a51d4
--- /dev/null
+++ b/src/gs-payment-method.h
@@ -0,0 +1,49 @@
+ /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_PAYMENT_METHOD_H
+#define __GS_PAYMENT_METHOD_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PAYMENT_METHOD (gs_payment_method_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPaymentMethod, gs_payment_method, GS, PAYMENT_METHOD, GObject)
+
+GsPaymentMethod        *gs_payment_method_new                  (void);
+
+const gchar    *gs_payment_method_get_description      (GsPaymentMethod        *method);
+void            gs_payment_method_set_description      (GsPaymentMethod        *method,
+                                                        const gchar            *description);
+
+const gchar    *gs_payment_method_get_metadata_item    (GsPaymentMethod        *method,
+                                                        const gchar            *key);
+void            gs_payment_method_add_metadata         (GsPaymentMethod        *method,
+                                                        const gchar            *key,
+                                                        const gchar            *value);
+
+G_END_DECLS
+
+#endif /* __GS_PAYMENT_METHOD_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-plugin-loader.c b/src/gs-plugin-loader.c
index 25c7cdd..c960d24 100644
--- a/src/gs-plugin-loader.c
+++ b/src/gs-plugin-loader.c
@@ -107,6 +107,15 @@ typedef gboolean    (*GsPluginAuthFunc)            (GsPlugin       *plugin,
                                                         GsAuth         *auth,
                                                         GCancellable   *cancellable,
                                                         GError         **error);
+typedef gboolean        (*GsPluginPaymentMethodFunc)   (GsPlugin       *plugin,
+                                                        GList          **payment_methods,
+                                                        GCancellable   *cancellable,
+                                                        GError         **error);
+typedef gboolean        (*GsPluginPurchaseFunc)        (GsPlugin       *plugin,
+                                                        GsApp          *app,
+                                                        GsPrice        *price,
+                                                        GCancellable   *cancellable,
+                                                        GError         **error);
 typedef gboolean        (*GsPluginRefineFunc)          (GsPlugin       *plugin,
                                                         GsAppList      *list,
                                                         GsPluginRefineFlags flags,
@@ -147,6 +156,8 @@ typedef struct {
        GsApp                           *app;
        GsReview                        *review;
        GsAuth                          *auth;
+       GList                           *payment_methods;
+       GsPrice                         *price;
 } GsPluginLoaderAsyncState;
 
 static void
@@ -160,6 +171,10 @@ gs_plugin_loader_free_async_state (GsPluginLoaderAsyncState *state)
                g_object_unref (state->auth);
        if (state->review != NULL)
                g_object_unref (state->review);
+       if (state->payment_methods != NULL)
+               g_list_free_full (state->payment_methods, g_object_unref);
+       if (state->price != NULL)
+               g_object_unref (state->price);
        if (state->file != NULL)
                g_object_unref (state->file);
        if (state->list != NULL)
@@ -2833,6 +2848,136 @@ gs_plugin_loader_review_action_thread_cb (GTask *task,
        g_task_return_boolean (task, TRUE);
 }
 
+static void
+gs_plugin_loader_add_payment_methods_thread_cb (GTask *task,
+                                               gpointer object,
+                                               gpointer task_data,
+                                               GCancellable *cancellable)
+{
+       GError *error = NULL;
+       GsPluginLoaderAsyncState *state = (GsPluginLoaderAsyncState *) task_data;
+       GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+       GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+       GsPlugin *plugin;
+       GsPluginPaymentMethodFunc plugin_func = NULL;
+       gboolean anything_ran = FALSE;
+       gboolean exists;
+       gboolean ret;
+       guint i;
+
+       /* run each plugin */
+       for (i = 0; i < priv->plugins->len; i++) {
+               g_autoptr(AsProfileTask) ptask = NULL;
+               g_autoptr(GError) error_local = NULL;
+
+               plugin = g_ptr_array_index (priv->plugins, i);
+               if (!gs_plugin_get_enabled (plugin))
+                       continue;
+               if (g_cancellable_set_error_if_cancelled (cancellable, &error))
+                       g_task_return_error (task, error);
+
+               exists = g_module_symbol (gs_plugin_get_module (plugin),
+                                         state->function_name,
+                                         (gpointer *) &plugin_func);
+               if (!exists)
+                       continue;
+               ptask = as_profile_start (priv->profile,
+                                         "GsPlugin::%s(%s)",
+                                         gs_plugin_get_name (plugin),
+                                         state->function_name);
+               gs_plugin_loader_action_start (plugin_loader, plugin, FALSE);
+               ret = plugin_func (plugin, &state->payment_methods,
+                                  cancellable, &error_local);
+               gs_plugin_loader_action_stop (plugin_loader, plugin);
+               if (!ret) {
+                       g_warning ("failed to call %s on %s: %s",
+                                  state->function_name,
+                                  gs_plugin_get_name (plugin),
+                                  error_local->message);
+                       continue;
+               }
+               anything_ran = TRUE;
+               gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
+       }
+
+       /* nothing ran */
+       if (!anything_ran) {
+               g_set_error (&error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_NOT_SUPPORTED,
+                            "no plugin could handle %s",
+                            state->function_name);
+               g_task_return_error (task, error);
+       }
+
+       g_task_return_boolean (task, TRUE);
+}
+
+static void
+gs_plugin_loader_app_purchase_thread_cb (GTask *task,
+                                        gpointer object,
+                                        gpointer task_data,
+                                        GCancellable *cancellable)
+{
+       GError *error = NULL;
+       GsPluginLoaderAsyncState *state = (GsPluginLoaderAsyncState *) task_data;
+       GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+       GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+       GsPlugin *plugin;
+       GsPluginPurchaseFunc plugin_func = NULL;
+       gboolean anything_ran = FALSE;
+       gboolean exists;
+       gboolean ret;
+       guint i;
+
+       /* run each plugin */
+       for (i = 0; i < priv->plugins->len; i++) {
+               g_autoptr(AsProfileTask) ptask = NULL;
+               g_autoptr(GError) error_local = NULL;
+
+               plugin = g_ptr_array_index (priv->plugins, i);
+               if (!gs_plugin_get_enabled (plugin))
+                       continue;
+               if (g_cancellable_set_error_if_cancelled (cancellable, &error))
+                       g_task_return_error (task, error);
+
+               exists = g_module_symbol (gs_plugin_get_module (plugin),
+                                         state->function_name,
+                                         (gpointer *) &plugin_func);
+               if (!exists)
+                       continue;
+               ptask = as_profile_start (priv->profile,
+                                         "GsPlugin::%s(%s)",
+                                         gs_plugin_get_name (plugin),
+                                         state->function_name);
+               gs_plugin_loader_action_start (plugin_loader, plugin, FALSE);
+               ret = plugin_func (plugin, state->app, state->price,
+                                  cancellable, &error_local);
+               gs_plugin_loader_action_stop (plugin_loader, plugin);
+               if (!ret) {
+                       g_warning ("failed to call %s on %s: %s",
+                                  state->function_name,
+                                  gs_plugin_get_name (plugin),
+                                  error_local->message);
+                       continue;
+               }
+               anything_ran = TRUE;
+               gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
+       }
+
+       /* nothing ran */
+       if (!anything_ran) {
+               g_set_error (&error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_NOT_SUPPORTED,
+                            "no plugin could handle %s",
+                            state->function_name);
+               g_task_return_error (task, error);
+       }
+
+       g_task_return_boolean (task, TRUE);
+}
+
 static gboolean
 load_install_queue (GsPluginLoader *plugin_loader, GError **error)
 {
@@ -3074,6 +3219,64 @@ gs_plugin_loader_app_action_async (GsPluginLoader *plugin_loader,
        g_task_run_in_thread (task, gs_plugin_loader_app_action_thread_cb);
 }
 
+/**
+ * gs_plugin_loader_add_payment_methods_async:
+ **/
+void
+gs_plugin_loader_add_payment_methods_async (GsPluginLoader *plugin_loader,
+                                           GCancellable *cancellable,
+                                           GAsyncReadyCallback callback,
+                                           gpointer user_data)
+{
+       GsPluginLoaderAsyncState *state;
+       g_autoptr(GTask) task = NULL;
+
+       g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader));
+       g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+       /* save state */
+       state = g_slice_new0 (GsPluginLoaderAsyncState);
+       state->function_name = "gs_plugin_add_payment_methods";
+
+       /* run in a thread */
+       task = g_task_new (plugin_loader, cancellable, callback, user_data);
+       g_task_set_task_data (task, state, (GDestroyNotify) gs_plugin_loader_free_async_state);
+       g_task_run_in_thread (task, gs_plugin_loader_add_payment_methods_thread_cb);
+}
+
+/**
+ * gs_plugin_loader_app_purchase_async:
+ **/
+void
+gs_plugin_loader_app_purchase_async (GsPluginLoader *plugin_loader,
+                                     GsApp *app,
+                                     GsPrice *price,
+                                     GCancellable *cancellable,
+                                     GAsyncReadyCallback callback,
+                                     gpointer user_data)
+{
+       GsPluginLoaderAsyncState *state;
+       g_autoptr(GTask) task = NULL;
+
+       g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader));
+       g_return_if_fail (GS_IS_APP (app));
+       g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+       /* save state */
+       state = g_slice_new0 (GsPluginLoaderAsyncState);
+       state->app = g_object_ref (app);
+       state->price = g_object_ref (price);
+       state->function_name = "gs_plugin_app_purchase";
+
+       /* run in a thread */
+       task = g_task_new (plugin_loader, cancellable, callback, user_data);
+       g_task_set_task_data (task, state, (GDestroyNotify) gs_plugin_loader_free_async_state);
+       g_task_run_in_thread (task, gs_plugin_loader_app_purchase_thread_cb);
+}
+
+/**
+ * gs_plugin_loader_review_action_async:
+ **/
 void
 gs_plugin_loader_review_action_async (GsPluginLoader *plugin_loader,
                                      GsApp *app,
diff --git a/src/gs-plugin-loader.h b/src/gs-plugin-loader.h
index 1214d53..9487de2 100644
--- a/src/gs-plugin-loader.h
+++ b/src/gs-plugin-loader.h
@@ -218,6 +218,19 @@ gboolean    gs_plugin_loader_app_action_finish     (GsPluginLoader *plugin_loader,
 gboolean        gs_plugin_loader_review_action_finish  (GsPluginLoader *plugin_loader,
                                                         GAsyncResult   *res,
                                                         GError         **error);
+void            gs_plugin_loader_add_payment_methods_async (GsPluginLoader *plugin_loader,
+                                                            GCancellable *cancellable,
+                                                            GAsyncReadyCallback callback,
+                                                            gpointer user_data);
+void            gs_plugin_loader_app_purchase_async    (GsPluginLoader *plugin_loader,
+                                                        GsApp          *app,
+                                                        GsPrice        *price,
+                                                        GCancellable   *cancellable,
+                                                        GAsyncReadyCallback callback,
+                                                        gpointer        user_data);
+gboolean        gs_plugin_loader_app_purchase_finish   (GsPluginLoader *plugin_loader,
+                                                        GAsyncResult   *res,
+                                                        GError         **error);
 void            gs_plugin_loader_review_action_async   (GsPluginLoader *plugin_loader,
                                                         GsApp          *app,
                                                         GsReview       *review,
diff --git a/src/gs-plugin-vfuncs.h b/src/gs-plugin-vfuncs.h
index 0473515..9e7ddb7 100644
--- a/src/gs-plugin-vfuncs.h
+++ b/src/gs-plugin-vfuncs.h
@@ -40,6 +40,8 @@
 #include "gs-app.h"
 #include "gs-app-list.h"
 #include "gs-category.h"
+#include "gs-payment-method.h"
+#include "gs-price.h"
 
 G_BEGIN_DECLS
 
@@ -497,6 +499,45 @@ gboolean    gs_plugin_update_cancel                (GsPlugin       *plugin,
                                                         GError         **error);
 
 /**
+ * gs_plugin_add_payment_methods:
+ * @plugin: a #GsPlugin
+ * @payment_methods: list to add payment methods to.
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get the available payment methods for purchasing apps.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean        gs_plugin_add_payment_methods          (GsPlugin       *plugin,
+                                                        GList          **payment_methods,
+                                                        GCancellable   *cancellable,
+                                                        GError         **error);
+
+/**
+ * gs_plugin_app_purchase:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @price: a #GsPrice
+ * @payment_method: (allow-none): a #GsPaymentMethod, or %NULL
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Purchase the application.
+ *
+ * NOTE: Once the action is complete, the plugin must set the new state of @app
+ * to %AS_APP_STATE_AVAILABLE.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean        gs_plugin_app_purchase                 (GsPlugin               *plugin,
+                                                        GsApp                  *app,
+                                                        GsPrice                *price,
+                                                        GsPaymentMethod        *payment_method,
+                                                        GCancellable           *cancellable,
+                                                        GError                 **error);
+
+/**
  * gs_plugin_app_install:
  * @plugin: a #GsPlugin
  * @app: a #GsApp
@@ -913,6 +954,7 @@ gboolean     gs_plugin_auth_register                (GsPlugin       *plugin,
                                                         GCancellable   *cancellable,
                                                         GError         **error);
 
+
 G_END_DECLS
 
 #endif /* __GS_PLUGIN_VFUNCS_H */
diff --git a/src/gs-plugin.h b/src/gs-plugin.h
index 61c9966..89a36a4 100644
--- a/src/gs-plugin.h
+++ b/src/gs-plugin.h
@@ -32,6 +32,7 @@
 #include "gs-app-list.h"
 #include "gs-auth.h"
 #include "gs-category.h"
+#include "gs-price.h"
 
 G_BEGIN_DECLS
 
diff --git a/src/gs-price.c b/src/gs-price.c
new file mode 100644
index 0000000..17f29a7
--- /dev/null
+++ b/src/gs-price.c
@@ -0,0 +1,174 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gs-price.h"
+
+struct _GsPrice
+{
+       GObject                  parent_instance;
+
+       gdouble                  amount;
+       gchar                   *currency;
+};
+
+G_DEFINE_TYPE (GsPrice, gs_price, G_TYPE_OBJECT)
+
+/**
+ * gs_price_get_amount:
+ * @price: a #GsPrice
+ *
+ * Get the amount of money in this price.
+ *
+ * Returns: The amount of money in this price, e.g. 0.99
+ */
+gdouble
+gs_price_get_amount (GsPrice *price)
+{
+       g_return_val_if_fail (GS_IS_PRICE (price), 0);
+       return price->amount;
+}
+
+/**
+ * gs_price_set_amount:
+ * @price: a #GsPrice
+ * @amount: The amount of this price, e.g. 0.99
+ *
+ * Set the amount of money in this price.
+ */
+void
+gs_price_set_amount (GsPrice *price, gdouble amount)
+{
+       g_return_if_fail (GS_IS_PRICE (price));
+       price->amount = amount;
+}
+
+/**
+ * gs_price_get_currency:
+ * @price: a #GsPrice
+ *
+ * Get the currency a price is using.
+ *
+ * Returns: an ISO 4217 currency code for this price, e.g. "USD"
+ */
+const gchar *
+gs_price_get_currency (GsPrice *price)
+{
+       g_return_val_if_fail (GS_IS_PRICE (price), NULL);
+       return price->currency;
+}
+
+/**
+ * gs_price_set_currency:
+ * @price: a #GsPrice
+ * @currency: An ISO 4217 currency code, e.g. "USD"
+ *
+ * Set the currency this price is using.
+ */
+void
+gs_price_set_currency (GsPrice *price, const gchar *currency)
+{
+       g_return_if_fail (GS_IS_PRICE (price));
+       g_free (price->currency);
+       price->currency = g_strdup (currency);
+}
+
+/**
+ * gs_price_to_string:
+ * @price: a #GsPrice
+ *
+ * Convert a price object to a human readable string.
+ *
+ * Returns: A human readable string for this price, e.g. "US$0.99"
+ */
+gchar *
+gs_price_to_string (GsPrice *price)
+{
+       g_return_val_if_fail (GS_IS_PRICE (price), NULL);
+
+       if (g_strcmp0 (price->currency, "AUD") == 0) {
+               return g_strdup_printf (_("A$%.2f"), price->amount);
+       } else if (g_strcmp0 (price->currency, "CAD") == 0) {
+               return g_strdup_printf (_("C$%.2f"), price->amount);
+       } else if (g_strcmp0 (price->currency, "CNY") == 0) {
+               return g_strdup_printf (_("CN¥%.2f"), price->amount);
+       } else if (g_strcmp0 (price->currency, "EUR") == 0) {
+               return g_strdup_printf (_("€%.2f"), price->amount);
+       } else if (g_strcmp0 (price->currency, "GBP") == 0) {
+               return g_strdup_printf (_("£%.2f"), price->amount);
+       } else if (g_strcmp0 (price->currency, "JPY") == 0) {
+               return g_strdup_printf (_("¥%.2f"), price->amount);
+       } else if (g_strcmp0 (price->currency, "NZD") == 0) {
+               return g_strdup_printf (_("NZ$%.2f"), price->amount);
+       } else if (g_strcmp0 (price->currency, "RUB") == 0) {
+               return g_strdup_printf (_("₽%.2f"), price->amount);
+       } else if (g_strcmp0 (price->currency, "USD") == 0) {
+               return g_strdup_printf (_("US$%.2f"), price->amount);
+       } else {
+               return g_strdup_printf (_("%s %f"), price->currency, price->amount);
+       }
+}
+
+static void
+gs_price_finalize (GObject *object)
+{
+       GsPrice *price = GS_PRICE (object);
+
+       g_free (price->currency);
+
+       G_OBJECT_CLASS (gs_price_parent_class)->finalize (object);
+}
+
+static void
+gs_price_class_init (GsPriceClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = gs_price_finalize;
+}
+
+static void
+gs_price_init (GsPrice *price)
+{
+}
+
+/**
+ * gs_price_new:
+ * @amount: The amount of this price, e.g. 0.99
+ * @currency: An ISO 4217 currency code, e.g. "USD"
+ *
+ * Creates a new price object.
+ *
+ * Return value: a new #GsPrice object.
+ **/
+GsPrice *
+gs_price_new (gdouble amount, const gchar *currency)
+{
+       GsPrice *price;
+       price = g_object_new (GS_TYPE_PRICE, NULL);
+       price->amount = amount;
+       price->currency = currency;
+       return GS_PRICE (price);
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-price.h b/src/gs-price.h
new file mode 100644
index 0000000..aab8677
--- /dev/null
+++ b/src/gs-price.h
@@ -0,0 +1,50 @@
+ /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_PRICE_H
+#define __GS_PRICE_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PRICE (gs_price_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPrice, gs_price, GS, PRICE, GObject)
+
+GsPrice                *gs_price_new                           (gdouble         amount,
+                                                        const gchar    *currency);
+
+gdouble                 gs_price_get_amount                    (GsPrice        *price);
+void            gs_price_set_amount                    (GsPrice        *price,
+                                                        gdouble         amount);
+
+const gchar    *gs_price_get_currency                  (GsPrice        *price);
+void            gs_price_set_currency                  (GsPrice        *price,
+                                                        const gchar    *currency);
+
+gchar          *gs_price_to_string                     (GsPrice        *price);
+
+G_END_DECLS
+
+#endif /* __GS_PRICE_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-purchase-dialog.c b/src/gs-purchase-dialog.c
new file mode 100644
index 0000000..2e332a1
--- /dev/null
+++ b/src/gs-purchase-dialog.c
@@ -0,0 +1,58 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gs-purchase-dialog.h"
+
+struct _GsPurchaseDialog
+{
+       GtkDialog        parent_instance;
+};
+
+G_DEFINE_TYPE (GsPurchaseDialog, gs_purchase_dialog, GTK_TYPE_DIALOG)
+
+static void
+gs_purchase_dialog_init (GsPurchaseDialog *dialog)
+{
+       gtk_widget_init_template (GTK_WIDGET (dialog));
+}
+
+static void
+gs_purchase_dialog_class_init (GsPurchaseDialogClass *klass)
+{
+       GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+       gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/Software/gs-purchase-dialog.ui");
+}
+
+GtkWidget *
+gs_purchase_dialog_new (void)
+{
+       return GTK_WIDGET (g_object_new (GS_TYPE_PURCHASE_DIALOG,
+                                        "use-header-bar", TRUE,
+                                        NULL));
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-purchase-dialog.h b/src/gs-purchase-dialog.h
new file mode 100644
index 0000000..e91ff21
--- /dev/null
+++ b/src/gs-purchase-dialog.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2016 Canonical Ltd.
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GS_PURCHASE_DIALOG_H
+#define GS_PURCHASE_DIALOG_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PURCHASE_DIALOG (gs_purchase_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPurchaseDialog, gs_purchase_dialog, GS, PURCHASE_DIALOG, GtkDialog)
+
+GtkWidget      *gs_purchase_dialog_new         (void);
+
+G_END_DECLS
+
+#endif /* GS_PURCHASE_DIALOG_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-purchase-dialog.ui b/src/gs-purchase-dialog.ui
new file mode 100644
index 0000000..453275d
--- /dev/null
+++ b/src/gs-purchase-dialog.ui
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3 -->
+<interface>
+  <requires lib="gtk+" version="3.10"/>
+  <template class="GsReviewDialog" parent="GtkDialog">
+    <action-widgets>
+      <action-widget response="cancel">cancel_button</action-widget>
+      <action-widget response="ok">purchase_button</action-widget>
+    </action-widgets>
+    <property name="can_focus">False</property>
+    <property name="title" translatable="yes" comments="Translators: Title of the dialog box where users 
purchase apps.">Purchase</property>
+    <property name="modal">True</property>
+    <property name="default_width">600</property>
+    <property name="default_height">300</property>
+    <property name="destroy_with_parent">True</property>
+    <property name="type_hint">dialog</property>
+    <property name="use_header_bar">1</property>
+    <child internal-child="headerbar">
+      <object class="GtkHeaderBar">
+        <property name="show_close_button">False</property>
+        <child>
+          <object class="GtkButton" id="cancel_button">
+            <property name="label" translatable="yes">_Cancel</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+          </object>
+          <packing>
+            <property name="pack-type">start</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="purchase_button">
+            <property name="label" translatable="yes" comments="Translators: A button to purchase an 
app.">_Purchase</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="use_underline">True</property>
+            <style>
+              <class name="suggested-action"/>
+            </style>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child internal-child="vbox">
+      <object class="GtkBox" id="dialog-vbox">
+        <property name="can_focus">False</property>
+        <property name="margin_start">40</property>
+        <property name="margin_end">40</property>
+        <property name="margin_top">25</property>
+        <property name="margin_bottom">25</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">9</property>
+        <child internal-child="action_area">
+          <object class="GtkButtonBox" id="dialog-action_area1">
+            <property name="can_focus">False</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkSizeGroup" id="sizegroup_folder_buttons">
+    <property name="ignore-hidden">False</property>
+    <property name="mode">horizontal</property>
+    <widgets>
+      <widget name="cancel_button"/>
+      <widget name="post_button"/>
+    </widgets>
+  </object>
+</interface>
diff --git a/src/gs-shell-details.c b/src/gs-shell-details.c
index 68bf27d..4e25818 100644
--- a/src/gs-shell-details.c
+++ b/src/gs-shell-details.c
@@ -37,6 +37,7 @@
 #include "gs-review-histogram.h"
 #include "gs-review-dialog.h"
 #include "gs-review-row.h"
+#include "gs-price.h"
 
 /* the number of reviews to show before clicking the 'More Reviews' button */
 #define SHOW_NR_REVIEWS_INITIAL                4
@@ -230,6 +231,7 @@ gs_shell_details_switch_to (GsPage *page, gboolean scroll_up)
        GtkWidget *widget;
        GtkStyleContext *sc;
        GtkAdjustment *adj;
+       GPtrArray *prices;
 
        if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_DETAILS) {
                g_warning ("Called switch_to(details) when in mode %s",
@@ -258,6 +260,29 @@ gs_shell_details_switch_to (GsPage *page, gboolean scroll_up)
 
        /* install button */
        switch (state) {
+       case AS_APP_STATE_PURCHASABLE:
+               gtk_widget_set_visible (self->button_install, TRUE);
+               gtk_widget_set_sensitive (self->button_install, TRUE);
+               gtk_style_context_add_class (gtk_widget_get_style_context (self->button_install), 
"suggested-action");
+               prices = gs_app_get_prices (self->app);
+               if (prices->len > 0) {
+                       GsPrice *price = g_ptr_array_index (prices, 0);
+                       g_autofree gchar *text = NULL;
+                       text = gs_price_to_string (price);
+                       gtk_button_set_label (GTK_BUTTON (self->button_install), text);
+               } else {
+                       /* TRANSLATORS: button text in the header when an application
+                        * can be purchased */
+                       gtk_button_set_label (GTK_BUTTON (self->button_install), _("_Buy"));
+               }
+               break;
+       case AS_APP_STATE_PURCHASING:
+               gtk_widget_set_visible (self->button_install, TRUE);
+               gtk_widget_set_sensitive (self->button_install, FALSE);
+               /* TRANSLATORS: button text in the header when an application
+                * is in the process of being purchased */
+               gtk_button_set_label (GTK_BUTTON (self->button_install), _("_Buying"));
+               break;
        case AS_APP_STATE_AVAILABLE:
        case AS_APP_STATE_AVAILABLE_LOCAL:
                gtk_widget_set_visible (self->button_install, TRUE);
@@ -357,6 +382,8 @@ gs_shell_details_switch_to (GsPage *page, gboolean scroll_up)
                        gtk_style_context_remove_class (gtk_widget_get_style_context (self->button_remove), 
"destructive-action");
                        gtk_button_set_label (GTK_BUTTON (self->button_remove), _("_Cancel"));
                        break;
+               case AS_APP_STATE_PURCHASABLE:
+               case AS_APP_STATE_PURCHASING:
                case AS_APP_STATE_AVAILABLE_LOCAL:
                case AS_APP_STATE_AVAILABLE:
                case AS_APP_STATE_INSTALLING:
@@ -409,6 +436,11 @@ gs_shell_details_refresh_progress (GsShellDetails *self)
                gtk_label_set_label (GTK_LABEL (self->label_progress_status),
                                     _("Installing"));
                break;
+       case AS_APP_STATE_PURCHASING:
+               gtk_widget_set_visible (self->label_progress_status, TRUE);
+               gtk_label_set_label (GTK_LABEL (self->label_progress_status),
+                                    _("Buying"));
+               break;
        default:
                gtk_widget_set_visible (self->label_progress_status, FALSE);
                break;
@@ -416,6 +448,7 @@ gs_shell_details_refresh_progress (GsShellDetails *self)
 
        /* percentage bar */
        switch (state) {
+       case AS_APP_STATE_PURCHASING:
        case AS_APP_STATE_INSTALLING:
                percentage = gs_app_get_progress (self->app);
                if (percentage > 0) {
@@ -1544,6 +1577,11 @@ gs_shell_details_app_install_button_cb (GtkWidget *widget, GsShellDetails *self)
        g_autoptr(GList) addons = NULL;
        g_autoptr(GCancellable) cancellable = g_cancellable_new ();
 
+       if (gs_app_get_state (self->app) == AS_APP_STATE_PURCHASABLE) {
+               gs_page_purchase_app (GS_PAGE (self), self->app, self->cancellable);
+               return;
+       }
+
        /* Mark ticked addons to be installed together with the app */
        addons = gtk_container_get_children (GTK_CONTAINER (self->list_box_addons));
        for (l = addons; l; l = l->next) {
diff --git a/src/gs-shell-search.c b/src/gs-shell-search.c
index 07c9c53..1bfbac8 100644
--- a/src/gs-shell-search.c
+++ b/src/gs-shell-search.c
@@ -69,8 +69,11 @@ gs_shell_search_app_row_clicked_cb (GsAppRow *app_row,
                                    GsShellSearch *self)
 {
        GsApp *app;
+
        app = gs_app_row_get_app (app_row);
-       if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE)
+       if (gs_app_get_state (app) == AS_APP_STATE_PURCHASABLE)
+               gs_page_purchase_app (GS_PAGE (self), app, self->cancellable);
+       else if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE)
                gs_page_install_app (GS_PAGE (self), app, self->cancellable);
        else if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED)
                gs_page_remove_app (GS_PAGE (self), app, self->cancellable);
diff --git a/src/gs-utils.h b/src/gs-utils.h
index 884ab91..6cb7a8b 100644
--- a/src/gs-utils.h
+++ b/src/gs-utils.h
@@ -65,6 +65,7 @@ gboolean       gs_utils_strv_fnmatch          (gchar          **strv,
 GDesktopAppInfo *gs_utils_get_desktop_app_info (const gchar    *id);
 gboolean        gs_utils_rmtree                (const gchar    *directory,
                                                 GError         **error);
+
 G_END_DECLS
 
 #endif /* __GS_UTILS_H */
diff --git a/src/plugins/gs-plugin-dummy.c b/src/plugins/gs-plugin-dummy.c
index 67df02b..591268d 100644
--- a/src/plugins/gs-plugin-dummy.c
+++ b/src/plugins/gs-plugin-dummy.c
@@ -145,13 +145,31 @@ gs_plugin_add_search (GsPlugin *plugin,
                      GError **error)
 {
        GsPluginData *priv = gs_plugin_get_data (plugin);
-       g_autoptr(GsApp) app = NULL;
+       g_autoptr(GsApp) app = NULL, app2 = NULL;
        g_autoptr(AsIcon) ic = NULL;
 
        /* we're very specific */
        if (g_strcmp0 (values[0], "chiron") != 0)
                return TRUE;
 
+       /* use a generic stock icon */
+       ic = as_icon_new ();
+       as_icon_set_kind (ic, AS_ICON_KIND_STOCK);
+       as_icon_set_name (ic, "drive-harddisk");
+
+       /* add a live updatable normal application */
+       app2 = gs_app_new ("chiron-paid.desktop");
+       gs_app_set_name (app2, GS_APP_QUALITY_NORMAL, "Chiron (paid)");
+       gs_app_set_summary (app2, GS_APP_QUALITY_NORMAL, "A teaching application");
+       gs_app_add_price (app2, 100, "USD");
+       gs_app_add_icon (app2, ic);
+       gs_app_set_size_installed (app2, 42 * 1024 * 1024);
+       gs_app_set_size_download (app2, 50 * 1024 * 1024);
+       gs_app_set_kind (app2, AS_APP_KIND_DESKTOP);
+       gs_app_set_state (app2, AS_APP_STATE_PURCHASABLE);
+       gs_app_set_management_plugin (app2, gs_plugin_get_name (plugin));
+       gs_app_list_add (list, app2);
+
        /* does the app already exist? */
        app = gs_plugin_cache_lookup (plugin, "chiron");
        if (app != NULL) {
@@ -491,7 +509,9 @@ gs_plugin_add_category_apps (GsPlugin *plugin,
                             GCancellable *cancellable,
                             GError **error)
 {
-       g_autoptr(GsApp) app = gs_app_new ("chiron.desktop");
+       g_autoptr(GsApp) app = NULL, app2 = NULL;
+
+       app = gs_app_new ("chiron.desktop");
        gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron");
        gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "View and use virtual machines");
        gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, "http://www.box.org";);
@@ -501,6 +521,19 @@ gs_plugin_add_category_apps (GsPlugin *plugin,
        gs_app_set_kind (app, AS_APP_KIND_DESKTOP);
        gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
        gs_app_list_add (list, app);
+
+       app2 = gs_app_new ("chiron-paid.desktop");
+       gs_app_set_name (app2, GS_APP_QUALITY_NORMAL, "Expensive App");
+       gs_app_set_summary (app2, GS_APP_QUALITY_NORMAL, "An app that costs you money");
+       gs_app_add_price (app2, 100, "USD");
+       gs_app_set_url (app2, AS_URL_KIND_HOMEPAGE, "http://www.example.com";);
+       gs_app_set_kind (app2, AS_APP_KIND_DESKTOP);
+       gs_app_set_state (app2, AS_APP_STATE_PURCHASABLE);
+       gs_app_set_pixbuf (app2, gdk_pixbuf_new_from_file 
("/usr/share/icons/hicolor/48x48/apps/chiron.desktop.png", NULL));
+       gs_app_set_kind (app2, AS_APP_KIND_DESKTOP);
+       gs_app_set_management_plugin (app2, gs_plugin_get_name (plugin));
+       gs_app_list_add (list, app2);
+
        return TRUE;
 }
 
@@ -618,6 +651,43 @@ gs_plugin_update_cancel (GsPlugin *plugin, GsApp *app,
        return TRUE;
 }
 
+/**
+ * gs_plugin_add_payment_methods:
+ */
+gboolean
+gs_plugin_add_payment_methods (GsPlugin *plugin,
+                              GList **payment_methods,
+                              GCancellable *cancellable,
+                              GError **error)
+{
+       GsPaymentMethod *method;
+       g_debug ("Adding payment methods");
+       method = gs_payment_method_new ();
+       gs_payment_method_set_description (method, "Test Payment Method");
+       gs_payment_method_add_metadata (method, "card-number", "0000 0000 0000 0000");
+       *payment_methods = g_list_append (*payment_methods, method);
+       return TRUE;
+}
+
+/**
+ * gs_plugin_app_purchase:
+ */
+gboolean
+gs_plugin_app_purchase (GsPlugin *plugin,
+                       GsApp *app,
+                       GsPrice *price,
+                       GsPaymentMethod *payment_method,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       g_debug ("Purchasing app");
+       gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+       return TRUE;
+}
+
+/**
+ * gs_plugin_review_submit:
+ */
 gboolean
 gs_plugin_review_submit (GsPlugin *plugin,
                         GsApp *app,


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]