Merge lp:~alecu/unity-scope-click/refunds-previews into lp:unity-scope-click

Proposed by Alejandro J. Cura
Status: Merged
Approved by: dobey
Approved revision: 328
Merged at revision: 327
Proposed branch: lp:~alecu/unity-scope-click/refunds-previews
Merge into: lp:unity-scope-click
Prerequisite: lp:~alecu/unity-scope-click/split-close-preview
Diff against target: 742 lines (+438/-23)
9 files modified
libclickscope/click/pay.cpp (+33/-3)
libclickscope/click/pay.h (+4/-3)
libclickscope/click/preview.cpp (+138/-4)
libclickscope/click/preview.h (+35/-3)
libclickscope/tests/test_preview.cpp (+199/-6)
scope/clickapps/apps-scope.cpp (+6/-0)
scope/clickstore/store-query.cpp (+13/-4)
scope/clickstore/store-query.h (+1/-0)
scope/clickstore/store-scope.cpp (+9/-0)
To merge this branch: bzr merge lp:~alecu/unity-scope-click/refunds-previews
Reviewer Review Type Date Requested Status
dobey (community) Approve
Charles Kerr (community) Approve
PS Jenkins bot continuous-integration Approve
Review via email: mp+257444@code.launchpad.net

Commit message

Show the "Refund" button and call the pay-service when clicked

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
324. By Alejandro J. Cura

Do not use the refundable time for equality comparison

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Charles Kerr (charlesk) wrote :

I think the branch needs to be resynced with trunk before silo.

Individual comments inline. Overall, looks good. I added a few optional suggestions but didn't see any showstoppers.

Revision history for this message
Charles Kerr (charlesk) :
review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
325. By Alejandro J. Cura

Merged trunk in

326. By Alejandro J. Cura

Fixes suggested by charles' review

327. By Alejandro J. Cura

Fix broken tests

Revision history for this message
Alejandro J. Cura (alecu) wrote :

Thanks for the thorough review!

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
dobey (dobey) wrote :

Some commentary in line.

review: Needs Information
Revision history for this message
Charles Kerr (charlesk) wrote :

Thanks for making the minor tweaks I suggested :)

Still LGTM overall, I agree with dobey's suggestion and it got me thinking about another issue, comments inline.

review: Approve
328. By Alejandro J. Cura

Don't call parse_timestamp twice in a row.

Revision history for this message
Alejandro J. Cura (alecu) wrote :

Thanks for the reviews, fixed as suggested.

Revision history for this message
dobey (dobey) :
review: Approve
329. By Alejandro J. Cura

Fixed the type of the timestamp stored in result variant

330. By Alejandro J. Cura

Call libpay.refund() from the qt thread

331. By Alejandro J. Cura

Add more logging before and after calling libpay

332. By Alejandro J. Cura

Initialize pay package before starting a refund

333. By Alejandro J. Cura

Now correctly setting up the pay package

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'libclickscope/click/pay.cpp'
2--- libclickscope/click/pay.cpp 2015-04-10 13:13:46 +0000
3+++ libclickscope/click/pay.cpp 2015-05-29 21:20:25 +0000
4@@ -82,7 +82,16 @@
5 namespace pay {
6
7 bool operator==(const Purchase& lhs, const Purchase& rhs) {
8- return lhs.name == rhs.name && lhs.refundable_until == rhs.refundable_until;
9+ return lhs.name == rhs.name;
10+}
11+
12+Package& Package::instance() {
13+ static Package the_instance;
14+ return the_instance;
15+}
16+
17+Package::Package() : impl(new Private())
18+{
19 }
20
21 Package::Package(const QSharedPointer<click::web::Client>& client) :
22@@ -101,6 +110,16 @@
23 }
24 }
25
26+bool Package::refund(const std::string& pkg_name)
27+{
28+ if (!running) {
29+ qDebug() << "pay service starting";
30+ setup_pay_service();
31+ }
32+ qDebug() << "actually calling refund";
33+ return pay_package_item_start_refund(impl->pay_package, pkg_name.c_str());
34+}
35+
36 bool Package::verify(const std::string& pkg_name)
37 {
38 typedef std::pair<std::string, bool> _PurchasedTuple;
39@@ -167,8 +186,12 @@
40 const json::Value item = root[i];
41 if (item[JsonKeys::state].asString() == PURCHASE_STATE_COMPLETE) {
42 auto package_name = item[JsonKeys::package_name].asString();
43+ qDebug() << "parsing:" << package_name.c_str();
44 auto refundable_until_value = item[JsonKeys::refundable_until];
45- Purchase p(package_name, parse_timestamp(refundable_until_value));
46+ qDebug() << "refundable until:" << refundable_until_value.asString().c_str();
47+ auto refundable_parsed = parse_timestamp(refundable_until_value);
48+ qDebug() << "parsed:" << refundable_parsed;
49+ Purchase p(package_name, refundable_parsed);
50 purchases.insert(p);
51 }
52 }
53@@ -195,10 +218,17 @@
54
55 void Package::setup_pay_service()
56 {
57- impl->pay_package = pay_package_new(Package::NAME);
58+ qDebug() << "new package";
59+ PayPackage* newpkg = pay_package_new(Package::NAME);
60+ qDebug() << "got package:" << newpkg;
61+ qDebug() << "about to set it on impl:" << impl.isNull();
62+ fprintf(stderr, "and the package is at: %p\n", impl->pay_package);
63+ impl->pay_package = newpkg;
64+ qDebug() << "installing observer";
65 pay_package_item_observer_install(impl->pay_package,
66 pay_verification_observer,
67 this);
68+ qDebug() << "Flag we are running";
69 running = true;
70 }
71
72
73=== modified file 'libclickscope/click/pay.h'
74--- libclickscope/click/pay.h 2015-04-10 13:13:46 +0000
75+++ libclickscope/click/pay.h 2015-05-29 21:20:25 +0000
76@@ -92,21 +92,22 @@
77 public:
78 constexpr static const char* NAME{"click-scope"};
79
80- Package() = default;
81+ Package();
82 Package(const QSharedPointer<click::web::Client>& client);
83 virtual ~Package();
84
85 virtual bool verify(const std::string& pkg_name);
86 virtual click::web::Cancellable get_purchases(std::function<void(const PurchaseSet& purchased_apps)> callback);
87-
88+ virtual bool refund(const std::string& pkg_name);
89 static std::string get_base_url();
90+ static Package& instance();
91
92 protected:
93 virtual void setup_pay_service();
94 virtual void pay_package_verify(const std::string& pkg_name);
95
96 struct Private;
97- std::shared_ptr<pay::Package::Private> impl;
98+ QScopedPointer<pay::Package::Private> impl;
99
100 bool running = false;
101 QSharedPointer<click::web::Client> client;
102
103=== modified file 'libclickscope/click/preview.cpp'
104--- libclickscope/click/preview.cpp 2015-04-10 14:24:10 +0000
105+++ libclickscope/click/preview.cpp 2015-05-29 21:20:25 +0000
106@@ -36,6 +36,7 @@
107 #include <click/dbus_constants.h>
108 #include <click/departments-db.h>
109 #include <click/utils.h>
110+#include <click/pay.h>
111
112 #include <boost/algorithm/string/replace.hpp>
113
114@@ -148,10 +149,16 @@
115 << " given with download_url" << QString::fromStdString(download_url);
116 return new UninstalledPreview(result, client, depts, nam);
117 }
118+ } else if (metadict.count(click::Preview::Actions::CANCEL_PURCHASE_UNINSTALLED) != 0) {
119+ return new CancelPurchasePreview(result, false);
120+ } else if (metadict.count(click::Preview::Actions::CANCEL_PURCHASE_INSTALLED) != 0) {
121+ return new CancelPurchasePreview(result, true);
122 } else if (metadict.count(click::Preview::Actions::UNINSTALL_CLICK) != 0) {
123 return new UninstallConfirmationPreview(result);
124 } else if (metadict.count(click::Preview::Actions::CONFIRM_UNINSTALL) != 0) {
125 return new UninstallingPreview(result, client, nam);
126+ } else if (metadict.count(click::Preview::Actions::CONFIRM_CANCEL_PURCHASE) != 0) {
127+ return new CancellingPurchasePreview(result, client, nam);
128 } else if (metadict.count(click::Preview::Actions::RATED) != 0) {
129 return new InstalledPreview(result, metadata, client, depts);
130 } else if (metadict.count(click::Preview::Actions::SHOW_UNINSTALLED) != 0) {
131@@ -522,6 +529,16 @@
132 return widgets;
133 }
134
135+bool PreviewStrategy::isRefundable() const
136+{
137+ time_t refundable_until = 0;
138+ if (result.contains("refundable_until")) {
139+ refundable_until = result["refundable_until"].get_int64_t();
140+ }
141+ time_t now = time(NULL);
142+ // refund button is not shown if less than ten seconds left
143+ return refundable_until >= (now + 10);
144+}
145
146 // class DownloadErrorPreview
147
148@@ -747,10 +764,17 @@
149 }
150 if (manifest.removable)
151 {
152- builder.add_tuple({
153- {"id", scopes::Variant(click::Preview::Actions::UNINSTALL_CLICK)},
154- {"label", scopes::Variant(_("Uninstall"))}
155- });
156+ if (!isRefundable()) {
157+ builder.add_tuple({
158+ {"id", scopes::Variant(click::Preview::Actions::UNINSTALL_CLICK)},
159+ {"label", scopes::Variant(_("Uninstall"))}
160+ });
161+ } else {
162+ builder.add_tuple({
163+ {"id", scopes::Variant(click::Preview::Actions::CANCEL_PURCHASE_INSTALLED)},
164+ {"label", scopes::Variant(_("Cancel Purchase"))}
165+ });
166+ }
167 }
168 if (!uri.empty() || manifest.removable) {
169 buttons.add_attribute_value("actions", builder.end());
170@@ -852,6 +876,69 @@
171 return widgets;
172 }
173
174+// class CancelPurchasePreview
175+
176+CancelPurchasePreview::CancelPurchasePreview(const unity::scopes::Result& result, bool installed)
177+ : PreviewStrategy(result), installed(installed)
178+{
179+}
180+
181+CancelPurchasePreview::~CancelPurchasePreview()
182+{
183+}
184+
185+scopes::PreviewWidgetList CancelPurchasePreview::build_widgets()
186+{
187+ scopes::PreviewWidgetList widgets;
188+
189+ scopes::PreviewWidget confirmation("confirmation", "text");
190+
191+ std::string title = result["title"].get_string();
192+ // TRANSLATORS: Do NOT translate ${title} here.
193+ std::string message =
194+ _("Are you sure you want to cancel the purchase of '${title}'? The app will be uninstalled.");
195+
196+ boost::replace_first(message, "${title}", title);
197+ confirmation.add_attribute_value("text", scopes::Variant(message));
198+ widgets.push_back(confirmation);
199+
200+ scopes::PreviewWidget buttons("buttons", "actions");
201+ scopes::VariantBuilder builder;
202+
203+ auto action_no = installed ? click::Preview::Actions::SHOW_INSTALLED
204+ : click::Preview::Actions::SHOW_UNINSTALLED;
205+
206+ builder.add_tuple({
207+ {"id", scopes::Variant(action_no)},
208+ {"label", scopes::Variant(_("No"))}
209+ });
210+ builder.add_tuple({
211+ {"id", scopes::Variant(click::Preview::Actions::CONFIRM_CANCEL_PURCHASE)},
212+ {"label", scopes::Variant(_("Yes, cancel purchase"))}
213+ });
214+
215+ buttons.add_attribute_value("actions", builder.end());
216+ widgets.push_back(buttons);
217+
218+ scopes::PreviewWidget policy("policy", "text");
219+ policy.add_attribute_value("title", scopes::Variant{_("Returns and cancellation policy")});
220+ policy.add_attribute_value("text", scopes::Variant{
221+ _("When purchasing an app in the Ubuntu Store, you can cancel the charge within 15 minutes "
222+ "after installation. If the cancel period has passed, we recommend contacting the app "
223+ "developer directly for a refund.\n"
224+ "You can find the developer’s contact information listed on the app’s preview page in the "
225+ "Ubuntu Store.\n"
226+ "Keep in mind that you cannot cancel the purchasing process of an app more than once.")});
227+ widgets.push_back(policy);
228+
229+ return widgets;
230+}
231+
232+void CancelPurchasePreview::run(unity::scopes::PreviewReplyProxy const& reply)
233+{
234+ // NOTE: no need to populateDetails() here.
235+ reply->push(build_widgets());
236+}
237
238 // class UninstallConfirmationPreview
239
240@@ -975,6 +1062,13 @@
241 {"download_url", scopes::Variant(details.download_url)},
242 {"download_sha512", scopes::Variant(details.download_sha512)},
243 });
244+ if (isRefundable()) {
245+ builder.add_tuple(
246+ {
247+ {"id", scopes::Variant(click::Preview::Actions::CANCEL_PURCHASE_UNINSTALLED)},
248+ {"label", scopes::Variant(_("Cancel Purchase"))},
249+ });
250+ }
251 buttons.add_attribute_value("actions", builder.end());
252 oa_client.register_account_login_item(buttons,
253 scopes::OnlineAccountClient::PostLoginAction::ContinueActivation,
254@@ -1028,4 +1122,44 @@
255 }
256
257
258+// class CancellingPurchasePreview : public UninstallingPreview
259+
260+CancellingPurchasePreview::CancellingPurchasePreview(const unity::scopes::Result& result,
261+ const QSharedPointer<click::web::Client>& client,
262+ const QSharedPointer<click::network::AccessManager>& nam)
263+ : UninstallingPreview(result, client, nam)
264+{
265+}
266+
267+CancellingPurchasePreview::~CancellingPurchasePreview()
268+{
269+}
270+
271+void CancellingPurchasePreview::run(unity::scopes::PreviewReplyProxy const& reply)
272+{
273+ qDebug() << "in CancellingPurchasePreview::run, calling cancel_purchase";
274+ cancel_purchase();
275+ qDebug() << "in CancellingPurchasePreview::run, calling UninstallingPreview::run()";
276+ UninstallingPreview::run(reply);
277+}
278+
279+void CancellingPurchasePreview::cancel_purchase()
280+{
281+ auto package_name = result["name"].get_string();
282+ qDebug() << "Will cancel the purchase of:" << package_name.c_str();
283+
284+ std::promise<bool> refund_promise;
285+ std::future<bool> refund_future = refund_promise.get_future();
286+
287+ run_under_qt([&refund_promise, package_name]() {
288+ qDebug() << "Calling refund for:" << package_name.c_str();
289+ auto ret = pay::Package::instance().refund(package_name);
290+ qDebug() << "Refund returned:" << ret;
291+ refund_promise.set_value(ret);
292+ });
293+ bool finished = refund_future.get();
294+ qDebug() << "Finished refund:" << finished;
295+}
296+
297+
298 } // namespace click
299
300=== modified file 'libclickscope/click/preview.h'
301--- libclickscope/click/preview.h 2015-04-10 14:24:10 +0000
302+++ libclickscope/click/preview.h 2015-05-29 21:20:25 +0000
303@@ -98,8 +98,11 @@
304 constexpr static const char* PIN_TO_LAUNCHER{"pin_to_launcher"};
305 constexpr static const char* UNINSTALL_CLICK{"uninstall_click"};
306 constexpr static const char* CONFIRM_UNINSTALL{"confirm_uninstall"};
307+ constexpr static const char* CANCEL_PURCHASE_UNINSTALLED{"cancel_purchase_uninstalled"};
308+ constexpr static const char* CANCEL_PURCHASE_INSTALLED{"cancel_purchase_installed"};
309 constexpr static const char* SHOW_UNINSTALLED{"show_uninstalled"};
310 constexpr static const char* SHOW_INSTALLED{"show_installed"};
311+ constexpr static const char* CONFIRM_CANCEL_PURCHASE{"confirm_cancel_purchase"};
312 constexpr static const char* OPEN_ACCOUNTS{"open_accounts"};
313 constexpr static const char* RATED{"rated"};
314 };
315@@ -151,6 +154,7 @@
316 virtual scopes::PreviewWidget build_updates_table(const PackageDetails& details);
317 virtual std::string build_whats_new(const PackageDetails& details);
318 virtual void run_under_qt(const std::function<void ()> &task);
319+ virtual bool isRefundable() const;
320
321 scopes::Result result;
322 QSharedPointer<click::web::Client> client;
323@@ -209,10 +213,9 @@
324
325 protected:
326 void getApplicationUri(const Manifest& manifest, std::function<void(const std::string&)> callback);
327-
328+ scopes::PreviewWidgetList createButtons(const std::string& uri,
329+ const click::Manifest& manifest);
330 private:
331- static scopes::PreviewWidgetList createButtons(const std::string& uri,
332- const click::Manifest& manifest);
333 scopes::ActionMetadata metadata;
334 };
335
336@@ -236,6 +239,20 @@
337 virtual scopes::PreviewWidgetList purchasingWidgets(const PackageDetails &);
338 };
339
340+class CancelPurchasePreview : public PreviewStrategy
341+{
342+public:
343+ CancelPurchasePreview(const unity::scopes::Result& result, bool installed);
344+
345+ virtual ~CancelPurchasePreview();
346+
347+ void run(unity::scopes::PreviewReplyProxy const& reply) override;
348+
349+protected:
350+ scopes::PreviewWidgetList build_widgets();
351+ bool installed;
352+};
353+
354 class UninstallConfirmationPreview : public PreviewStrategy
355 {
356 public:
357@@ -284,6 +301,21 @@
358
359 };
360
361+class CancellingPurchasePreview : public UninstallingPreview
362+{
363+public:
364+ CancellingPurchasePreview(const unity::scopes::Result& result,
365+ const QSharedPointer<click::web::Client>& client,
366+ const QSharedPointer<click::network::AccessManager>& nam);
367+
368+ virtual ~CancellingPurchasePreview();
369+
370+ void run(unity::scopes::PreviewReplyProxy const& reply) override;
371+
372+protected:
373+ void cancel_purchase();
374+};
375+
376 } // namespace click
377
378 #endif
379
380=== modified file 'libclickscope/tests/test_preview.cpp'
381--- libclickscope/tests/test_preview.cpp 2015-03-26 18:49:42 +0000
382+++ libclickscope/tests/test_preview.cpp 2015-05-29 21:20:25 +0000
383@@ -27,6 +27,8 @@
384 * files in the program, then also delete it here.
385 */
386
387+#include <time.h>
388+
389 #include <unity/scopes/testing/MockPreviewReply.h>
390 #include <unity/scopes/testing/Result.h>
391
392@@ -34,6 +36,7 @@
393 #include <click/preview.h>
394 #include <fake_json.h>
395 #include <click/index.h>
396+#include <click/interface.h>
397 #include <click/reviews.h>
398 #include <boost/locale/time_zone.hpp>
399
400@@ -100,6 +103,7 @@
401 using click::PreviewStrategy::build_updates_table;
402 using click::PreviewStrategy::build_whats_new;
403 using click::PreviewStrategy::populateDetails;
404+ using click::PreviewStrategy::isRefundable;
405 };
406
407 class PreviewsBaseTest : public Test
408@@ -330,15 +334,15 @@
409 }
410 };
411
412-class FakeUninstalledPreview : public click::UninstalledPreview {
413+class FakeBaseUninstalledPreview : public click::UninstalledPreview {
414 std::string object_path;
415 public:
416 std::unique_ptr<FakeDownloader> fake_downloader;
417- FakeUninstalledPreview(const std::string& object_path,
418- const unity::scopes::Result& result,
419- const QSharedPointer<click::web::Client>& client,
420- const std::shared_ptr<click::DepartmentsDb>& depts,
421- const QSharedPointer<click::network::AccessManager>& nam)
422+ FakeBaseUninstalledPreview(const std::string& object_path,
423+ const unity::scopes::Result& result,
424+ const QSharedPointer<click::web::Client>& client,
425+ const std::shared_ptr<click::DepartmentsDb>& depts,
426+ const QSharedPointer<click::network::AccessManager>& nam)
427 : click::UninstalledPreview(result, client, depts, nam), object_path(object_path),
428 fake_downloader(new FakeDownloader(object_path, nam))
429 {
430@@ -356,10 +360,22 @@
431 details_callback(details);
432 reviews_callback({}, click::Reviews::Error::NoError);
433 }
434+};
435+
436+class FakeUninstalledPreview : public FakeBaseUninstalledPreview {
437+public:
438 MOCK_METHOD1(uninstalledActionButtonWidgets, scopes::PreviewWidgetList (const click::PackageDetails &details));
439 MOCK_METHOD1(progressBarWidget, scopes::PreviewWidgetList(const std::string& object_path));
440+ FakeUninstalledPreview(const std::string& object_path,
441+ const unity::scopes::Result& result,
442+ const QSharedPointer<click::web::Client>& client,
443+ const std::shared_ptr<click::DepartmentsDb>& depts,
444+ const QSharedPointer<click::network::AccessManager>& nam)
445+ : FakeBaseUninstalledPreview(object_path, result, client, depts, nam) {
446+ }
447 };
448
449+
450 TEST_F(UninstalledPreviewTest, testDownloadInProgress) {
451 std::string fake_object_path = "/fake/object/path";
452
453@@ -385,3 +401,180 @@
454 preview.run(replyptr);
455 preview.fake_downloader->activate_callback();
456 }
457+
458+class FakeUninstalledRefundablePreview : FakeBaseUninstalledPreview {
459+public:
460+ FakeUninstalledRefundablePreview(const unity::scopes::Result& result,
461+ const QSharedPointer<click::web::Client>& client,
462+ const std::shared_ptr<click::DepartmentsDb>& depts,
463+ const QSharedPointer<click::network::AccessManager>& nam)
464+ : FakeBaseUninstalledPreview(std::string{""}, result, client, depts, nam){
465+ }
466+ using click::UninstalledPreview::uninstalledActionButtonWidgets;
467+ MOCK_CONST_METHOD0(isRefundable, bool());
468+};
469+
470+unity::scopes::VariantArray get_actions_from_widgets(const unity::scopes::PreviewWidgetList& widgets, int widget_number) {
471+ auto widget = *std::next(widgets.begin(), widget_number);
472+ return widget.attribute_values()["actions"].get_array();
473+}
474+
475+std::string get_action_from_widgets(const unity::scopes::PreviewWidgetList& widgets, int widget_number, int action_number) {
476+ auto actions = get_actions_from_widgets(widgets, widget_number);
477+ auto selected_action = actions.at(action_number).get_dict();
478+ return selected_action["id"].get_string();
479+}
480+
481+TEST_F(UninstalledPreviewTest, testIsRefundableButtonShown) {
482+ result["name"] = "fake_app_name";
483+ result["price"] = 2.99;
484+ result["purchased"] = true;
485+ FakeUninstalledRefundablePreview preview(result, client, depts, nam);
486+
487+ click::PackageDetails pkgdetails;
488+ EXPECT_CALL(preview, isRefundable()).Times(1)
489+ .WillOnce(Return(true));
490+ auto widgets = preview.uninstalledActionButtonWidgets(pkgdetails);
491+ ASSERT_EQ(get_action_from_widgets(widgets, 0, 1), "cancel_purchase_uninstalled");
492+}
493+
494+TEST_F(UninstalledPreviewTest, testIsRefundableButtonNotShown) {
495+ result["name"] = "fake_app_name";
496+ result["price"] = 2.99;
497+ result["purchased"] = true;
498+ FakeUninstalledRefundablePreview preview(result, client, depts, nam);
499+
500+ click::PackageDetails pkgdetails;
501+ EXPECT_CALL(preview, isRefundable()).Times(1)
502+ .WillOnce(Return(false));
503+ auto widgets = preview.uninstalledActionButtonWidgets(pkgdetails);
504+ ASSERT_EQ(get_actions_from_widgets(widgets, 0).size(), 1);
505+}
506+
507+class InstalledPreviewTest : public Test {
508+protected:
509+ unity::scopes::testing::Result result;
510+ unity::scopes::ActionMetadata metadata;
511+ unity::scopes::VariantMap metadict;
512+ QSharedPointer<click::web::Client> client;
513+ QSharedPointer<click::network::AccessManager> nam;
514+ std::shared_ptr<click::DepartmentsDb> depts;
515+
516+public:
517+ InstalledPreviewTest() : metadata("en_EN", "phone") {
518+ }
519+};
520+
521+class FakeInstalledRefundablePreview : public click::InstalledPreview {
522+public:
523+ FakeInstalledRefundablePreview(const unity::scopes::Result& result,
524+ const unity::scopes::ActionMetadata& metadata,
525+ const QSharedPointer<click::web::Client> client,
526+ const std::shared_ptr<click::DepartmentsDb> depts)
527+ : click::InstalledPreview(result, metadata, client, depts) {
528+
529+ }
530+ using click::InstalledPreview::createButtons;
531+ MOCK_CONST_METHOD0(isRefundable, bool());
532+};
533+
534+TEST_F(InstalledPreviewTest, testIsRefundableButtonShown) {
535+ FakeInstalledRefundablePreview preview(result, metadata, client, depts);
536+ EXPECT_CALL(preview, isRefundable()).Times(1)
537+ .WillOnce(Return(true));
538+ click::Manifest manifest;
539+ manifest.removable = true;
540+ auto widgets = preview.createButtons("fake uri", manifest);
541+ ASSERT_EQ(get_actions_from_widgets(widgets, 0).size(), 2);
542+ ASSERT_EQ(get_action_from_widgets(widgets, 0, 1), "cancel_purchase_installed");
543+}
544+
545+TEST_F(InstalledPreviewTest, testIsRefundableButtonNotShown) {
546+ FakeInstalledRefundablePreview preview(result, metadata, client, depts);
547+ EXPECT_CALL(preview, isRefundable()).Times(1)
548+ .WillOnce(Return(false));
549+ click::Manifest manifest;
550+ manifest.removable = true;
551+ auto widgets = preview.createButtons("fake uri", manifest);
552+ ASSERT_EQ(get_actions_from_widgets(widgets, 0).size(), 2);
553+ ASSERT_EQ(get_action_from_widgets(widgets, 0, 1), "uninstall_click");
554+}
555+
556+
557+class RefundableTest : public PreviewStrategyTest {
558+
559+};
560+
561+TEST_F(RefundableTest, testIsNotRefundableWhenFieldMissing) {
562+ FakeResult result{vm};
563+ FakePreview preview{result};
564+ ASSERT_FALSE(preview.isRefundable());
565+}
566+
567+TEST_F(RefundableTest, testIsNotRefundableWhenExpired) {
568+ FakeResult result{vm};
569+ time_t now = time(NULL);
570+ result["refundable_until"] = (int64_t) (now - 300);
571+ FakePreview preview{result};
572+ ASSERT_FALSE(preview.isRefundable());
573+}
574+
575+TEST_F(RefundableTest, testIsRefundable) {
576+ FakeResult result{vm};
577+ time_t now = time(NULL);
578+ result["refundable_until"] = (int64_t) (now + 300);
579+ FakePreview preview{result};
580+ ASSERT_TRUE(preview.isRefundable());
581+}
582+
583+TEST_F(RefundableTest, testIsNotRefundableWhenExpiringRealSoon) {
584+ FakeResult result{vm};
585+ time_t now = time(NULL);
586+ result["refundable_until"] = (int64_t) (now + 8);
587+ FakePreview preview{result};
588+ ASSERT_FALSE(preview.isRefundable());
589+}
590+
591+
592+class FakeCancelPurchasePreview : public click::CancelPurchasePreview {
593+public:
594+ FakeCancelPurchasePreview(const unity::scopes::Result& result, bool installed)
595+ : click::CancelPurchasePreview(result, installed) {
596+
597+ }
598+ using click::CancelPurchasePreview::build_widgets;
599+};
600+
601+class CancelPurchasePreviewTest : public PreviewsBaseTest {
602+
603+};
604+
605+TEST_F(CancelPurchasePreviewTest, testNoShowsInstalled)
606+{
607+ FakeResult result{vm};
608+ result["title"] = "fake app";
609+ FakeCancelPurchasePreview preview(result, true);
610+ auto widgets = preview.build_widgets();
611+ auto action = get_action_from_widgets(widgets, 1, 0);
612+ ASSERT_EQ(action, "show_installed");
613+}
614+
615+TEST_F(CancelPurchasePreviewTest, testNoShowsUninstalled)
616+{
617+ FakeResult result{vm};
618+ result["title"] = "fake app";
619+ FakeCancelPurchasePreview preview(result, false);
620+ auto widgets = preview.build_widgets();
621+ auto action = get_action_from_widgets(widgets, 1, 0);
622+ ASSERT_EQ(action, "show_uninstalled");
623+}
624+
625+TEST_F(CancelPurchasePreviewTest, testYesCancelsPurchase)
626+{
627+ FakeResult result{vm};
628+ result["title"] = "fake app";
629+ FakeCancelPurchasePreview preview(result, false);
630+ auto widgets = preview.build_widgets();
631+ auto action = get_action_from_widgets(widgets, 1, 1);
632+ ASSERT_EQ(action, "confirm_cancel_purchase");
633+}
634
635=== modified file 'scope/clickapps/apps-scope.cpp'
636--- scope/clickapps/apps-scope.cpp 2015-04-10 14:24:10 +0000
637+++ scope/clickapps/apps-scope.cpp 2015-05-29 21:20:25 +0000
638@@ -118,6 +118,12 @@
639 if (action_id == click::Preview::Actions::UNINSTALL_CLICK) {
640 activation->setHint(click::Preview::Actions::UNINSTALL_CLICK, unity::scopes::Variant(true));
641 activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
642+ } else if (action_id == click::Preview::Actions::CANCEL_PURCHASE_INSTALLED) {
643+ activation->setHint(click::Preview::Actions::CANCEL_PURCHASE_INSTALLED, unity::scopes::Variant(true));
644+ activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
645+ } else if (action_id == click::Preview::Actions::CANCEL_PURCHASE_UNINSTALLED) {
646+ activation->setHint(click::Preview::Actions::CANCEL_PURCHASE_UNINSTALLED, unity::scopes::Variant(true));
647+ activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
648 } else if (action_id == click::Preview::Actions::SHOW_INSTALLED) {
649 activation->setHint(click::Preview::Actions::SHOW_INSTALLED, unity::scopes::Variant(true));
650 activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
651
652=== modified file 'scope/clickstore/store-query.cpp'
653--- scope/clickstore/store-query.cpp 2015-03-18 14:00:53 +0000
654+++ scope/clickstore/store-query.cpp 2015-05-29 21:20:25 +0000
655@@ -293,7 +293,8 @@
656 ss << "☆ " << pkg.rating;
657 std::string rating{ss.str()};
658
659- bool purchased = false;
660+ bool was_purchased = false;
661+ time_t refundable_until = 0;
662 double cur_price{0.00};
663 auto suggested = impl->index.get_suggested_currency();
664 std::string currency = Configuration::get_currency(suggested);
665@@ -307,20 +308,27 @@
666 res["price"] = scopes::Variant(cur_price);
667 res[click::Query::ResultKeys::VERSION] = pkg.version;
668
669+
670+ qDebug() << "App:" << pkg.name.c_str() << ", price:" << cur_price;
671 if (cur_price > 0.00f) {
672 if (!Configuration::get_purchases_enabled()) {
673 // Don't show priced apps if flag not set
674 return;
675 }
676 // Check if the priced app was already purchased.
677- purchased = purchased_apps.count({pkg.name}) != 0;
678+ auto purchased = purchased_apps.find({pkg.name});
679+ was_purchased = purchased != purchased_apps.end();
680+ if (was_purchased) {
681+ refundable_until = purchased->refundable_until;
682+ }
683+ qDebug() << "was purchased?" << was_purchased << ", refundable_until:" << refundable_until;
684 }
685 if (installed != installedPackages.end()) {
686 res[click::Query::ResultKeys::INSTALLED] = true;
687- res[click::Query::ResultKeys::PURCHASED] = purchased;
688+ res[click::Query::ResultKeys::PURCHASED] = was_purchased;
689 price = _("✔ INSTALLED");
690 res[click::Query::ResultKeys::VERSION] = installed->version;
691- } else if (purchased) {
692+ } else if (was_purchased) {
693 res[click::Query::ResultKeys::PURCHASED] = true;
694 res[click::Query::ResultKeys::INSTALLED] = false;
695 price = _("✔ PURCHASED");
696@@ -336,6 +344,7 @@
697 }
698 }
699
700+ res[click::Query::ResultKeys::REFUNDABLE_UNTIL] = unity::scopes::Variant((int64_t)refundable_until);
701 res["price_area"] = price;
702 res["rating"] = rating;
703
704
705=== modified file 'scope/clickstore/store-query.h'
706--- scope/clickstore/store-query.h 2015-04-10 13:13:46 +0000
707+++ scope/clickstore/store-query.h 2015-05-29 21:20:25 +0000
708@@ -64,6 +64,7 @@
709 constexpr static const char* MAIN_SCREENSHOT{"main_screenshot"};
710 constexpr static const char* INSTALLED{"installed"};
711 constexpr static const char* PURCHASED{"purchased"};
712+ constexpr static const char* REFUNDABLE_UNTIL{"refundable_until"};
713 constexpr static const char* VERSION{"version"};
714 };
715
716
717=== modified file 'scope/clickstore/store-scope.cpp'
718--- scope/clickstore/store-scope.cpp 2015-04-10 14:24:10 +0000
719+++ scope/clickstore/store-scope.cpp 2015-05-29 21:20:25 +0000
720@@ -145,6 +145,12 @@
721 } else if (action_id == click::Preview::Actions::DOWNLOAD_COMPLETED) {
722 activation->setHint(click::Preview::Actions::DOWNLOAD_COMPLETED, unity::scopes::Variant(true));
723 activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
724+ } else if (action_id == click::Preview::Actions::CANCEL_PURCHASE_INSTALLED) {
725+ activation->setHint(click::Preview::Actions::CANCEL_PURCHASE_INSTALLED, unity::scopes::Variant(true));
726+ activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
727+ } else if (action_id == click::Preview::Actions::CANCEL_PURCHASE_UNINSTALLED) {
728+ activation->setHint(click::Preview::Actions::CANCEL_PURCHASE_UNINSTALLED, unity::scopes::Variant(true));
729+ activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
730 } else if (action_id == click::Preview::Actions::UNINSTALL_CLICK) {
731 activation->setHint(click::Preview::Actions::UNINSTALL_CLICK, unity::scopes::Variant(true));
732 activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
733@@ -157,6 +163,9 @@
734 } else if (action_id == click::Preview::Actions::CONFIRM_UNINSTALL) {
735 activation->setHint(click::Preview::Actions::CONFIRM_UNINSTALL, unity::scopes::Variant(true));
736 activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
737+ } else if (action_id == click::Preview::Actions::CONFIRM_CANCEL_PURCHASE) {
738+ activation->setHint(click::Preview::Actions::CONFIRM_CANCEL_PURCHASE, unity::scopes::Variant(true));
739+ activation->setStatus(unity::scopes::ActivationResponse::Status::ShowPreview);
740 } else if (action_id == click::Preview::Actions::RATED) {
741 scopes::VariantMap rating_info = metadata.scope_data().get_dict();
742 // Cast to int because widget gives us double, which is wrong.

Subscribers

People subscribed via source and target branches