Merge lp:~alecu/unity-scope-click/refunds-previews into lp:unity-scope-click
- refunds-previews
- Merge into trunk
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 |
Related bugs: |
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
Description of the change
PS Jenkins bot (ps-jenkins) wrote : | # |
- 324. By Alejandro J. Cura
-
Do not use the refundable time for equality comparison
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:324
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
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) : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:324
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
FAILURE: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
- 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
Alejandro J. Cura (alecu) wrote : | # |
Thanks for the thorough review!
PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:327
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
deb: http://
FAILURE: http://
SUCCESS: http://
deb: http://
Click here to trigger a rebuild:
http://
dobey (dobey) wrote : | # |
Some commentary in line.
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.
- 328. By Alejandro J. Cura
-
Don't call parse_timestamp twice in a row.
Alejandro J. Cura (alecu) wrote : | # |
Thanks for the reviews, fixed as suggested.
dobey (dobey) : | # |
- 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
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. |
FAILED: Continuous integration, rev:323 jenkins. qa.ubuntu. com/job/ unity-scope- click-ci/ 583/ jenkins. qa.ubuntu. com/job/ unity-scope- click-vivid- amd64-ci/ 39/console jenkins. qa.ubuntu. com/job/ unity-scope- click-vivid- armhf-ci/ 40/console
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/unity- scope-click- ci/583/ rebuild
http://