[gnome-software/wip/rancell/paid] Support purchasable apps.



commit 51b71a5565181bab31e55196a47dbdb4a955b495
Author: Robert Ancell <robert ancell canonical com>
Date:   Thu May 25 22:57:39 2017 +1200

    Support purchasable apps.

 lib/gs-app.c                    |   61 ++++++++++++-
 lib/gs-app.h                    |    6 +
 lib/gs-plugin-job-private.h     |    1 +
 lib/gs-plugin-job.c             |   32 +++++++
 lib/gs-plugin-job.h             |    3 +
 lib/gs-plugin-loader.c          |   13 +++
 lib/gs-plugin-types.h           |    2 +
 lib/gs-plugin-vfuncs.h          |   22 +++++
 lib/gs-plugin.c                 |    4 +
 lib/gs-price.c                  |  174 +++++++++++++++++++++++++++++++++++
 lib/gs-price.h                  |   50 ++++++++++
 lib/meson.build                 |    2 +
 plugins/dummy/gs-plugin-dummy.c |   22 +++++
 plugins/dummy/gs-self-test.c    |   29 ++++++-
 po/POTFILES.in                  |    1 +
 src/gs-details-page.c           |   14 +++
 src/gs-page.c                   |  192 ++++++++++++++++++++++++++++++++++++--
 src/gs-search-page.c            |    2 +-
 18 files changed, 616 insertions(+), 14 deletions(-)
---
diff --git a/lib/gs-app.c b/lib/gs-app.c
index aee3432..c6ba358 100644
--- a/lib/gs-app.c
+++ b/lib/gs-app.c
@@ -122,6 +122,7 @@ struct _GsApp
        GFile                   *local_file;
        AsContentRating         *content_rating;
        GdkPixbuf               *pixbuf;
+       GsPrice                 *price;
 };
 
 enum {
@@ -527,6 +528,10 @@ gs_app_to_string (GsApp *app)
                gs_app_kv_size (str, "size-installed", app->size_installed);
        if (app->size_download != 0)
                gs_app_kv_size (str, "size-download", app->size_download);
+       if (app->price != NULL)
+               gs_app_kv_printf (str, "price", "%s %.2f",
+                                 gs_price_get_currency (app->price),
+                                 gs_price_get_amount (app->price));
        if (app->related->len > 0)
                gs_app_kv_printf (str, "related", "%u", app->related->len);
        if (app->history->len > 0)
@@ -803,7 +808,8 @@ gs_app_set_state_internal (GsApp *app, AsAppState state)
                    state == AS_APP_STATE_AVAILABLE_LOCAL ||
                    state == AS_APP_STATE_UPDATABLE ||
                    state == AS_APP_STATE_UPDATABLE_LIVE ||
-                   state == AS_APP_STATE_UNAVAILABLE)
+                   state == AS_APP_STATE_UNAVAILABLE ||
+                   state == AS_APP_STATE_PURCHASABLE)
                        state_change_ok = TRUE;
                break;
        case AS_APP_STATE_INSTALLED:
@@ -841,6 +847,7 @@ gs_app_set_state_internal (GsApp *app, AsAppState state)
                /* removing has to go into an stable state */
                if (state == AS_APP_STATE_UNKNOWN ||
                    state == AS_APP_STATE_AVAILABLE ||
+                   state == AS_APP_STATE_PURCHASABLE ||
                    state == AS_APP_STATE_INSTALLED)
                        state_change_ok = TRUE;
                break;
@@ -870,6 +877,19 @@ gs_app_set_state_internal (GsApp *app, AsAppState state)
                    state == AS_APP_STATE_INSTALLING)
                        state_change_ok = TRUE;
                break;
+       case AS_APP_STATE_PURCHASABLE:
+               /* local has to go into an action state */
+               if (state == AS_APP_STATE_UNKNOWN ||
+                   state == AS_APP_STATE_PURCHASING)
+                       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_AVAILABLE ||
+                   state == AS_APP_STATE_PURCHASABLE)
+                       state_change_ok = TRUE;
+               break;
        default:
                g_warning ("state %s unhandled",
                           as_app_state_to_string (app->state));
@@ -1648,6 +1668,43 @@ gs_app_set_pixbuf (GsApp *app, GdkPixbuf *pixbuf)
        g_set_object (&app->pixbuf, pixbuf);
 }
 
+/**
+ * gs_app_get_price:
+ * @app: a #GsApp
+ *
+ * Gets the price required to purchase the application.
+ *
+ * Returns: (transfer none): a #GsPrice, or %NULL
+ *
+ * Since: 3.26
+ **/
+GsPrice *
+gs_app_get_price (GsApp *app)
+{
+       g_return_val_if_fail (GS_IS_APP (app), NULL);
+       return app->price;
+}
+
+/**
+ * gs_app_set_price:
+ * @app: a #GsApp
+ * @amount: the amount of this price, e.g. 0.99
+ * @currency: an ISO 4217 currency code, e.g. "USD"
+ *
+ * Sets a price required to purchase the application.
+ *
+ * Since: 3.26
+ **/
+void
+gs_app_set_price (GsApp *app, gdouble amount, const gchar *currency)
+{
+       g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&app->mutex);
+       g_return_if_fail (GS_IS_APP (app));
+       if (app->price != NULL)
+               g_object_unref (app->price);
+       app->price = gs_price_new (amount, currency);
+}
+
 typedef enum {
        GS_APP_VERSION_FIXUP_RELEASE            = 1,
        GS_APP_VERSION_FIXUP_DISTRO_SUFFIX      = 2,
@@ -3637,6 +3694,8 @@ gs_app_finalize (GObject *object)
                g_object_unref (app->content_rating);
        if (app->pixbuf != NULL)
                g_object_unref (app->pixbuf);
+       if (app->price != NULL)
+               g_object_unref (app->price);
 
        G_OBJECT_CLASS (gs_app_parent_class)->finalize (object);
 }
diff --git a/lib/gs-app.h b/lib/gs-app.h
index bddc068..45165f8 100644
--- a/lib/gs-app.h
+++ b/lib/gs-app.h
@@ -27,6 +27,8 @@
 #include <gdk-pixbuf/gdk-pixbuf.h>
 #include <appstream-glib.h>
 
+#include "gs-price.h"
+
 G_BEGIN_DECLS
 
 #define GS_TYPE_APP (gs_app_get_type ())
@@ -203,6 +205,10 @@ void                gs_app_set_management_plugin   (GsApp          *app,
 GdkPixbuf      *gs_app_get_pixbuf              (GsApp          *app);
 void            gs_app_set_pixbuf              (GsApp          *app,
                                                 GdkPixbuf      *pixbuf);
+GsPrice                *gs_app_get_price               (GsApp          *app);
+void            gs_app_set_price               (GsApp          *app,
+                                                gdouble         amount,
+                                                const gchar    *currency);
 GPtrArray      *gs_app_get_icons               (GsApp          *app);
 void            gs_app_add_icon                (GsApp          *app,
                                                 AsIcon         *icon);
diff --git a/lib/gs-plugin-job-private.h b/lib/gs-plugin-job-private.h
index 9771482..8792135 100644
--- a/lib/gs-plugin-job-private.h
+++ b/lib/gs-plugin-job-private.h
@@ -49,6 +49,7 @@ GsAppList             *gs_plugin_job_get_list                 (GsPluginJob    *self);
 GFile                  *gs_plugin_job_get_file                 (GsPluginJob    *self);
 GsCategory             *gs_plugin_job_get_category             (GsPluginJob    *self);
 AsReview               *gs_plugin_job_get_review               (GsPluginJob    *self);
+GsPrice                        *gs_plugin_job_get_price                (GsPluginJob    *self);
 gchar                  *gs_plugin_job_to_string                (GsPluginJob    *self);
 void                    gs_plugin_job_set_action               (GsPluginJob    *self,
                                                                 GsPluginAction  action);
diff --git a/lib/gs-plugin-job.c b/lib/gs-plugin-job.c
index 59589ff..eb8d66e 100644
--- a/lib/gs-plugin-job.c
+++ b/lib/gs-plugin-job.c
@@ -44,6 +44,7 @@ struct _GsPluginJob
        GFile                   *file;
        GsCategory              *category;
        AsReview                *review;
+       GsPrice                 *price;
 };
 
 enum {
@@ -61,6 +62,7 @@ enum {
        PROP_CATEGORY,
        PROP_REVIEW,
        PROP_MAX_RESULTS,
+       PROP_PRICE,
        PROP_LAST
 };
 
@@ -107,6 +109,10 @@ gs_plugin_job_to_string (GsPluginJob *self)
                g_string_append_printf (str, " with review=%s",
                                        as_review_get_id (self->review));
        }
+       if (self->price != NULL) {
+               g_autofree gchar *price_string = gs_price_to_string (self->price);
+               g_string_append_printf (str, " with price=%s", price_string);
+       }
        if (self->auth != NULL) {
                g_string_append_printf (str, " with auth=%s",
                                        gs_auth_get_provider_id (self->auth));
@@ -367,6 +373,20 @@ gs_plugin_job_get_review (GsPluginJob *self)
        return self->review;
 }
 
+void
+gs_plugin_job_set_price (GsPluginJob *self, GsPrice *price)
+{
+       g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+       g_set_object (&self->price, price);
+}
+
+GsPrice *
+gs_plugin_job_get_price (GsPluginJob *self)
+{
+       g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL);
+       return self->price;
+}
+
 static void
 gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec)
 {
@@ -409,6 +429,9 @@ gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSp
        case PROP_REVIEW:
                g_value_set_object (value, self->review);
                break;
+       case PROP_PRICE:
+               g_value_set_object (value, self->price);
+               break;
        case PROP_MAX_RESULTS:
                g_value_set_uint (value, self->max_results);
                break;
@@ -463,6 +486,9 @@ gs_plugin_job_set_property (GObject *obj, guint prop_id, const GValue *value, GP
        case PROP_MAX_RESULTS:
                gs_plugin_job_set_max_results (self, g_value_get_uint (value));
                break;
+       case PROP_PRICE:
+               gs_plugin_job_set_price (self, g_value_get_object (value));
+               break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
                break;
@@ -480,6 +506,7 @@ gs_plugin_job_finalize (GObject *obj)
        g_clear_object (&self->file);
        g_clear_object (&self->category);
        g_clear_object (&self->review);
+       g_clear_object (&self->price);
        G_OBJECT_CLASS (gs_plugin_job_parent_class)->finalize (obj);
 }
 
@@ -558,6 +585,11 @@ gs_plugin_job_class_init (GsPluginJobClass *klass)
                                   0, G_MAXUINT, 0,
                                   G_PARAM_READWRITE);
        g_object_class_install_property (object_class, PROP_MAX_RESULTS, pspec);
+
+       pspec = g_param_spec_object ("price", NULL, NULL,
+                                    GS_TYPE_PRICE,
+                                    G_PARAM_READWRITE);
+       g_object_class_install_property (object_class, PROP_PRICE, pspec);
 }
 
 static void
diff --git a/lib/gs-plugin-job.h b/lib/gs-plugin-job.h
index 557da8a..70a908e 100644
--- a/lib/gs-plugin-job.h
+++ b/lib/gs-plugin-job.h
@@ -28,6 +28,7 @@
 #include "gs-auth.h"
 #include "gs-category.h"
 #include "gs-plugin-types.h"
+#include "gs-price.h"
 
 G_BEGIN_DECLS
 
@@ -63,6 +64,8 @@ void           gs_plugin_job_set_category             (GsPluginJob    *self,
                                                         GsCategory     *category);
 void            gs_plugin_job_set_review               (GsPluginJob    *self,
                                                         AsReview       *review);
+void            gs_plugin_job_set_price                (GsPluginJob    *self,
+                                                        GsPrice        *price);
 
 #define                 gs_plugin_job_newv(a,...)              
GS_PLUGIN_JOB(g_object_new(GS_TYPE_PLUGIN_JOB, "action", a, __VA_ARGS__))
 
diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c
index 07664f6..efd60a2 100644
--- a/lib/gs-plugin-loader.c
+++ b/lib/gs-plugin-loader.c
@@ -131,6 +131,11 @@ typedef gboolean    (*GsPluginActionFunc)          (GsPlugin       *plugin,
                                                         GsApp          *app,
                                                         GCancellable   *cancellable,
                                                         GError         **error);
+typedef gboolean        (*GsPluginPurchaseFunc)        (GsPlugin       *plugin,
+                                                        GsApp          *app,
+                                                        GsPrice        *price,
+                                                        GCancellable   *cancellable,
+                                                        GError         **error);
 typedef gboolean        (*GsPluginReviewFunc)          (GsPlugin       *plugin,
                                                         GsApp          *app,
                                                         AsReview       *review,
@@ -572,6 +577,14 @@ gs_plugin_loader_call_vfunc (GsPluginLoaderHelper *helper,
                        ret = plugin_func (plugin, app, cancellable, &error_local);
                }
                break;
+       case GS_PLUGIN_ACTION_PURCHASE:
+               {
+                       GsPluginPurchaseFunc plugin_func = func;
+                       ret = plugin_func (plugin, app,
+                                          gs_plugin_job_get_price (helper->plugin_job),
+                                          cancellable, &error_local);
+               }
+               break;
        case GS_PLUGIN_ACTION_REVIEW_SUBMIT:
        case GS_PLUGIN_ACTION_REVIEW_UPVOTE:
        case GS_PLUGIN_ACTION_REVIEW_DOWNVOTE:
diff --git a/lib/gs-plugin-types.h b/lib/gs-plugin-types.h
index ad0e8dd..1b40159 100644
--- a/lib/gs-plugin-types.h
+++ b/lib/gs-plugin-types.h
@@ -265,6 +265,7 @@ typedef enum {
  * @GS_PLUGIN_ACTION_GET_RECENT:               Get the apps recently released
  * @GS_PLUGIN_ACTION_INITIALIZE:               Initialize the plugin
  * @GS_PLUGIN_ACTION_DESTROY:                  Destroy the plugin
+ * @GS_PLUGIN_ACTION_PURCHASE:                 Purchase an app
  *
  * The plugin action.
  **/
@@ -311,6 +312,7 @@ typedef enum {
        GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL,
        GS_PLUGIN_ACTION_INITIALIZE,
        GS_PLUGIN_ACTION_DESTROY,
+       GS_PLUGIN_ACTION_PURCHASE,
        /*< private >*/
        GS_PLUGIN_ACTION_LAST
 } GsPluginAction;
diff --git a/lib/gs-plugin-vfuncs.h b/lib/gs-plugin-vfuncs.h
index 4de2449..f8d48e7 100644
--- a/lib/gs-plugin-vfuncs.h
+++ b/lib/gs-plugin-vfuncs.h
@@ -40,6 +40,7 @@
 #include "gs-app.h"
 #include "gs-app-list.h"
 #include "gs-category.h"
+#include "gs-price.h"
 
 G_BEGIN_DECLS
 
@@ -561,6 +562,27 @@ gboolean    gs_plugin_update_cancel                (GsPlugin       *plugin,
                                                         GError         **error);
 
 /**
+ * gs_plugin_app_purchase:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @price: a #GsPrice
+ * @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,
+                                                        GCancellable   *cancellable,
+                                                        GError         **error);
+
+/**
  * gs_plugin_app_install:
  * @plugin: a #GsPlugin
  * @app: a #GsApp
diff --git a/lib/gs-plugin.c b/lib/gs-plugin.c
index f21faa5..46b4ee9 100644
--- a/lib/gs-plugin.c
+++ b/lib/gs-plugin.c
@@ -1758,6 +1758,8 @@ gs_plugin_action_to_function_name (GsPluginAction action)
                return "gs_plugin_initialize";
        if (action == GS_PLUGIN_ACTION_DESTROY)
                return "gs_plugin_destroy";
+       if (action == GS_PLUGIN_ACTION_PURCHASE)
+               return "gs_plugin_app_purchase";
        return NULL;
 }
 
@@ -1856,6 +1858,8 @@ gs_plugin_action_to_string (GsPluginAction action)
                return "initialize";
        if (action == GS_PLUGIN_ACTION_DESTROY)
                return "destroy";
+       if (action == GS_PLUGIN_ACTION_PURCHASE)
+               return "purchase";
        return NULL;
 }
 
diff --git a/lib/gs-price.c b/lib/gs-price.c
new file mode 100644
index 0000000..698d897
--- /dev/null
+++ b/lib/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 = g_strdup (currency);
+       return GS_PRICE (price);
+}
+
+/* vim: set noexpandtab: */
diff --git a/lib/gs-price.h b/lib/gs-price.h
new file mode 100644
index 0000000..aab8677
--- /dev/null
+++ b/lib/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/lib/meson.build b/lib/meson.build
index 95a5852..7d910ec 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -47,6 +47,7 @@ install_headers([
     'gs-plugin-event.h',
     'gs-plugin-types.h',
     'gs-plugin-vfuncs.h',
+    'gs-price.h',
     'gs-utils.h'
   ],
   subdir : 'gnome-software'
@@ -66,6 +67,7 @@ libgnomesoftware = static_library(
     'gs-plugin-job.c',
     'gs-plugin-loader.c',
     'gs-plugin-loader-sync.c',
+    'gs-price.c',
     'gs-test.c',
     'gs-utils.c',
   ],
diff --git a/plugins/dummy/gs-plugin-dummy.c b/plugins/dummy/gs-plugin-dummy.c
index f1e3e52..481e974 100644
--- a/plugins/dummy/gs-plugin-dummy.c
+++ b/plugins/dummy/gs-plugin-dummy.c
@@ -810,6 +810,28 @@ gs_plugin_update_cancel (GsPlugin *plugin, GsApp *app,
 }
 
 gboolean
+gs_plugin_app_purchase (GsPlugin *plugin,
+                       GsApp *app,
+                       GsPrice *price,
+                       GCancellable *cancellable,
+                       GError **error)
+{
+       g_debug ("Purchasing app");
+
+       /* purchase app */
+       if (g_strcmp0 (gs_app_get_id (app), "chiron-paid.desktop") == 0) {
+               gs_app_set_state (app, AS_APP_STATE_PURCHASING);
+               if (!gs_plugin_dummy_delay (plugin, app, 500, cancellable, error)) {
+                       gs_app_set_state_recover (app);
+                       return FALSE;
+               }
+               gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+       }
+
+       return TRUE;
+}
+
+gboolean
 gs_plugin_review_submit (GsPlugin *plugin,
                         GsApp *app,
                         AsReview *review,
diff --git a/plugins/dummy/gs-self-test.c b/plugins/dummy/gs-self-test.c
index 4f48dac..e4f9487 100644
--- a/plugins/dummy/gs-self-test.c
+++ b/plugins/dummy/gs-self-test.c
@@ -229,6 +229,30 @@ gs_plugins_dummy_updates_func (GsPluginLoader *plugin_loader)
 }
 
 static void
+gs_plugins_dummy_purchase_func (GsPluginLoader *plugin_loader)
+{
+       gboolean ret;
+       g_autoptr(GsApp) app = NULL;
+       g_autoptr(GError) error = NULL;
+       g_autoptr(GsAppList) list = NULL;
+       g_autoptr(GsPluginJob) plugin_job = NULL;
+
+       /* get the updates list */
+       app = gs_app_new ("chiron-paid.desktop");
+       gs_app_set_management_plugin (app, "dummy");
+       gs_app_set_state (app, AS_APP_STATE_PURCHASABLE);
+       gs_app_set_price (app, 100, "USD");
+       plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_PURCHASE,
+                                        "app", app,
+                                        NULL);
+       ret = gs_plugin_loader_job_action (plugin_loader, plugin_job, NULL, &error);
+       gs_test_flush_main_context ();
+       g_assert_no_error (error);
+       g_assert (ret);
+       g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_AVAILABLE);
+}
+
+static void
 gs_plugins_dummy_distro_upgrades_func (GsPluginLoader *plugin_loader)
 {
        GsApp *app;
@@ -684,7 +708,10 @@ main (int argc, char **argv)
        g_test_add_data_func ("/gnome-software/plugins/dummy/distro-upgrades",
                              plugin_loader,
                              (GTestDataFunc) gs_plugins_dummy_distro_upgrades_func);
-
+       g_test_add_data_func ("/gnome-software/plugins/dummy/purchase",
+                             plugin_loader,
+                             (GTestDataFunc) gs_plugins_dummy_purchase_func);
+;
        return g_test_run ();
 }
 
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 4f7a8ab..806489a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -42,6 +42,7 @@ src/gs-page.c
 lib/gs-plugin-loader.c
 src/gs-popular-tile.c
 src/gs-popular-tile.ui
+lib/gs-price.c
 src/gs-removal-dialog.c
 src/gs-removal-dialog.ui
 src/gs-review-dialog.c
diff --git a/src/gs-details-page.c b/src/gs-details-page.c
index 6879105..df736a0 100644
--- a/src/gs-details-page.c
+++ b/src/gs-details-page.c
@@ -235,6 +235,8 @@ gs_details_page_switch_to (GsPage *page, gboolean scroll_up)
        GsDetailsPage *self = GS_DETAILS_PAGE (page);
        AsAppState state;
        GtkWidget *widget;
+       GsPrice *price;
+       g_autofree gchar *text = NULL;
        GtkStyleContext *sc;
        GtkAdjustment *adj;
 
@@ -280,6 +282,16 @@ gs_details_page_switch_to (GsPage *page, gboolean scroll_up)
        case AS_APP_STATE_INSTALLING:
                gtk_widget_set_visible (self->button_install, FALSE);
                break;
+       case AS_APP_STATE_PURCHASABLE:
+               gtk_widget_set_visible (self->button_install, TRUE);
+               gtk_style_context_add_class (gtk_widget_get_style_context (self->button_install), 
"suggested-action");
+               price = gs_app_get_price (self->app);
+               text = gs_price_to_string (price);
+               gtk_button_set_label (GTK_BUTTON (self->button_install), text);
+               break;
+       case AS_APP_STATE_PURCHASING:
+               gtk_widget_set_visible (self->button_install, FALSE);
+               break;
        case AS_APP_STATE_UNKNOWN:
        case AS_APP_STATE_INSTALLED:
        case AS_APP_STATE_REMOVING:
@@ -371,6 +383,8 @@ gs_details_page_switch_to (GsPage *page, gboolean scroll_up)
                case AS_APP_STATE_REMOVING:
                case AS_APP_STATE_UNAVAILABLE:
                case AS_APP_STATE_UNKNOWN:
+               case AS_APP_STATE_PURCHASABLE:
+               case AS_APP_STATE_PURCHASING:
                        gtk_widget_set_visible (self->button_remove, FALSE);
                        break;
                default:
diff --git a/src/gs-page.c b/src/gs-page.c
index 234aca5..b005542 100644
--- a/src/gs-page.c
+++ b/src/gs-page.c
@@ -49,6 +49,7 @@ typedef struct {
        GtkWidget       *button_install;
        GsPluginAction   action;
        GsShellInteraction interaction;
+       GsPrice         *price;
 } GsPageHelper;
 
 static void
@@ -64,6 +65,8 @@ gs_page_helper_free (GsPageHelper *helper)
                g_object_unref (helper->cancellable);
        if (helper->soup_session != NULL)
                g_object_unref (helper->soup_session);
+       if (helper->price != NULL)
+               g_object_unref (helper->price);
        g_slice_free (GsPageHelper, helper);
 }
 
@@ -275,6 +278,136 @@ gs_page_set_header_end_widget (GsPage *page, GtkWidget *widget)
        g_set_object (&priv->header_end_widget, widget);
 }
 
+static void
+gs_page_app_purchased_cb (GObject *source,
+                          GAsyncResult *res,
+                          gpointer user_data);
+
+static void
+gs_page_purchase_authenticate_cb (GtkDialog *dialog,
+                                 GtkResponseType response_type,
+                                 GsPageHelper *helper)
+{
+       GsPagePrivate *priv = gs_page_get_instance_private (helper->page);
+       g_autoptr(GsPluginJob) plugin_job = NULL;
+
+       /* unmap the dialog */
+       gtk_widget_destroy (GTK_WIDGET (dialog));
+
+       if (response_type != GTK_RESPONSE_OK) {
+               gs_page_helper_free (helper);
+               return;
+       }
+       plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_PURCHASE,
+                                        "app", helper->app,
+                                        "failure-flags", GS_PLUGIN_FAILURE_FLAGS_USE_EVENTS,
+                                        NULL);
+       gs_plugin_loader_job_process_async (priv->plugin_loader, plugin_job,
+                                           helper->cancellable,
+                                           gs_page_app_purchased_cb,
+                                           helper);
+}
+
+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;
+       GCancellable *cancellable = helper->cancellable;
+       GsPagePrivate *priv = gs_page_get_instance_private (page);
+       gboolean ret;
+       g_autoptr(GsPluginJob) plugin_job = NULL;
+       g_autoptr(GError) error = NULL;
+
+       ret = gs_plugin_loader_job_action_finish (plugin_loader,
+                                                 res,
+                                                 &error);
+       if (g_error_matches (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_CANCELLED)) {
+               g_debug ("%s", error->message);
+               return;
+       }
+       if (!ret) {
+               /* try to authenticate then retry */
+               if (g_error_matches (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_AUTH_REQUIRED)) {
+                       g_autoptr(GError) error_local = NULL;
+                       GtkWidget *dialog;
+                       dialog = gs_auth_dialog_new (priv->plugin_loader,
+                                                    helper->app,
+                                                    gs_utils_get_error_value (error),
+                                                    &error_local);
+                       if (dialog == NULL) {
+                               g_warning ("%s", error_local->message);
+                               return;
+                       }
+                       gs_shell_modal_dialog_present (priv->shell, GTK_DIALOG (dialog));
+                       g_signal_connect (dialog, "response",
+                                         G_CALLBACK (gs_page_purchase_authenticate_cb),
+                                         g_steal_pointer (&helper));
+                       return;
+               }
+
+               g_warning ("failed to purchase %s: %s",
+                          gs_app_get_id (helper->app),
+                          error->message);
+               return;
+       }
+
+       if (gs_app_get_state (helper->app) != AS_APP_STATE_AVAILABLE) {
+               g_warning ("no plugin purchased %s: %s",
+                          gs_app_get_id (helper->app),
+                          error->message);
+               return;
+       }
+
+       /* now install */
+       plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+                                        "app", helper->app,
+                                        "failure-flags", GS_PLUGIN_FAILURE_FLAGS_USE_EVENTS,
+                                        NULL);
+       gs_plugin_loader_job_process_async (priv->plugin_loader,
+                                           plugin_job,
+                                           cancellable,
+                                           gs_page_app_installed_cb,
+                                           g_steal_pointer (&helper));
+}
+
+static void
+gs_page_install_purchase_response_cb (GtkDialog *dialog,
+                                     gint response,
+                                     GsPageHelper *helper)
+{
+       GsPagePrivate *priv = gs_page_get_instance_private (helper->page);
+       g_autoptr(GsPluginJob) plugin_job = NULL;
+
+       /* unmap the dialog */
+       gtk_widget_destroy (GTK_WIDGET (dialog));
+
+       /* not agreed */
+       if (response != GTK_RESPONSE_OK) {
+               gs_page_helper_free (helper);
+               return;
+       }
+       g_debug ("purchase %s", gs_app_get_id (helper->app));
+
+       plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_PURCHASE,
+                                        "app", helper->app,
+                                        "price", gs_app_get_price (helper->app),
+                                        "failure-flags", GS_PLUGIN_FAILURE_FLAGS_USE_EVENTS,
+                                        NULL);
+       gs_plugin_loader_job_process_async (priv->plugin_loader,
+                                           plugin_job,
+                                           helper->cancellable,
+                                           gs_page_app_purchased_cb,
+                                           helper);
+}
+
 void
 gs_page_install_app (GsPage *page,
                     GsApp *app,
@@ -283,11 +416,11 @@ gs_page_install_app (GsPage *page,
 {
        GsPagePrivate *priv = gs_page_get_instance_private (page);
        GsPageHelper *helper;
-       GtkResponseType response;
-       g_autoptr(GsPluginJob) plugin_job = NULL;
 
        /* probably non-free */
        if (gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE) {
+               GtkResponseType response;
+
                response = gs_app_notify_unavailable (app, gs_shell_get_window (priv->shell));
                if (response != GTK_RESPONSE_OK)
                        return;
@@ -299,15 +432,52 @@ gs_page_install_app (GsPage *page,
        helper->page = g_object_ref (page);
        helper->cancellable = g_object_ref (cancellable);
        helper->interaction = interaction;
-       plugin_job = gs_plugin_job_newv (helper->action,
-                                        "app", helper->app,
-                                        "failure-flags", GS_PLUGIN_FAILURE_FLAGS_USE_EVENTS,
-                                        NULL);
-       gs_plugin_loader_job_process_async (priv->plugin_loader,
-                                           plugin_job,
-                                           helper->cancellable,
-                                           gs_page_app_installed_cb,
-                                           helper);
+
+       /* need to purchase first */
+       if (gs_app_get_state (app) == AS_APP_STATE_PURCHASABLE) {
+               GtkWidget *dialog;
+               g_autofree gchar *title = NULL;
+               g_autofree gchar *message = NULL;
+               g_autofree gchar *price_text = NULL;
+
+               /* TRANSLATORS: this is a prompt message, and '%s' is an
+                * application summary, e.g. 'GNOME Clocks' */
+               title = g_strdup_printf (_("Are you sure you want to purchase %s?"),
+                                        gs_app_get_name (app));
+               price_text = gs_price_to_string (gs_app_get_price (app));
+               /* TRANSLATORS: longer dialog text */
+               message = g_strdup_printf (_("%s will be installed, and you will "
+                                            "be charged %s."),
+                                          gs_app_get_name (app), price_text);
+
+               dialog = gtk_message_dialog_new (gs_shell_get_window (priv->shell),
+                                                GTK_DIALOG_MODAL,
+                                                GTK_MESSAGE_QUESTION,
+                                                GTK_BUTTONS_CANCEL,
+                                                "%s", title);
+               gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+                                                         "%s", message);
+
+               /* 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_install_purchase_response_cb), helper);
+               gs_shell_modal_dialog_present (priv->shell, GTK_DIALOG (dialog));
+       } else {
+               g_autoptr(GsPluginJob) plugin_job = NULL;
+
+               plugin_job = gs_plugin_job_newv (helper->action,
+                                                "app", helper->app,
+                                                "failure-flags", GS_PLUGIN_FAILURE_FLAGS_USE_EVENTS,
+                                                NULL);
+               gs_plugin_loader_job_process_async (priv->plugin_loader,
+                                                   plugin_job,
+                                                   helper->cancellable,
+                                                   gs_page_app_installed_cb,
+                                                   helper);
+       }
 }
 
 static void
diff --git a/src/gs-search-page.c b/src/gs-search-page.c
index 518fa00..a97209e 100644
--- a/src/gs-search-page.c
+++ b/src/gs-search-page.c
@@ -71,7 +71,7 @@ gs_search_page_app_row_clicked_cb (GsAppRow *app_row,
 {
        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_AVAILABLE || gs_app_get_state (app) == 
AS_APP_STATE_PURCHASABLE)
                gs_page_install_app (GS_PAGE (self), app, GS_SHELL_INTERACTION_FULL,
                                     self->cancellable);
        else if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED)


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