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

Proposed by Alejandro J. Cura on 2015-04-24
Status: Merged
Approved by: dobey on 2015-05-18
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 on 2015-05-18
Charles Kerr (community) 2015-04-24 Approve on 2015-05-18
PS Jenkins bot continuous-integration Approve on 2015-05-15
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.
324. By Alejandro J. Cura on 2015-04-25

Do not use the refundable time for equality comparison

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.

Charles Kerr (charlesk) :
review: Approve
325. By Alejandro J. Cura on 2015-05-15

Merged trunk in

326. By Alejandro J. Cura on 2015-05-15

Fixes suggested by charles' review

327. By Alejandro J. Cura on 2015-05-15

Fix broken tests

Alejandro J. Cura (alecu) wrote :

Thanks for the thorough review!

dobey (dobey) wrote :

Some commentary in line.

review: Needs Information
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 on 2015-05-18

Don't call parse_timestamp twice in a row.

Alejandro J. Cura (alecu) wrote :

Thanks for the reviews, fixed as suggested.

dobey (dobey) :
review: Approve
329. By Alejandro J. Cura on 2015-05-20

Fixed the type of the timestamp stored in result variant

330. By Alejandro J. Cura on 2015-05-22

Call libpay.refund() from the qt thread

331. By Alejandro J. Cura on 2015-05-26

Add more logging before and after calling libpay

332. By Alejandro J. Cura on 2015-05-26

Initialize pay package before starting a refund

333. By Alejandro J. Cura on 2015-05-29

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