Merge lp:~mandel/unity-lens-music/musicstore-purchase into lp:unity-lens-music

Proposed by Manuel de la Peña
Status: Needs review
Proposed branch: lp:~mandel/unity-lens-music/musicstore-purchase
Merge into: lp:unity-lens-music
Diff against target: 1138 lines (+461/-199)
10 files modified
configure.ac (+1/-0)
debian/changelog (+8/-0)
debian/control (+2/-1)
src/Makefile.am (+2/-1)
src/album.vala (+1/-0)
src/musicstore-collection.vala (+13/-0)
src/musicstore-daemon.vala (+9/-3)
src/musicstore-scope.vala (+255/-13)
src/ubuntuone-webservices.vala (+104/-65)
tests/unit/test-ubuntuone-purchases.vala (+66/-116)
To merge this branch: bzr merge lp:~mandel/unity-lens-music/musicstore-purchase
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Needs Fixing
Unity Team Pending
Review via email: mp+156513@code.launchpad.net

Description of the change

Adds support for music purchase from the musicstore scope.

To post a comment you must log in.
135. By Manuel de la Peña

Add u1 icon to action.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:135
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~mandel/unity-lens-music/musicstore-purchase/+merge/156513/+edit-commit-message

http://jenkins.qa.ubuntu.com/job/unity-lens-music-ci/2/
Executed test runs:
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity-lens-music-raring-amd64-ci/2
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity-lens-music-raring-armhf-ci/2
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity-lens-music-raring-i386-ci/2

Click here to trigger a rebuild:
http://s-jenkins:8080/job/unity-lens-music-ci/2/rebuild

review: Needs Fixing (continuous-integration)
136. By Manuel de la Peña

Removed cached password.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:136
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~mandel/unity-lens-music/musicstore-purchase/+merge/156513/+edit-commit-message

http://jenkins.qa.ubuntu.com/job/unity-lens-music-ci/3/
Executed test runs:
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity-lens-music-raring-amd64-ci/3
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity-lens-music-raring-armhf-ci/3
    SUCCESS: http://jenkins.qa.ubuntu.com/job/unity-lens-music-raring-i386-ci/3

Click here to trigger a rebuild:
http://s-jenkins:8080/job/unity-lens-music-ci/3/rebuild

review: Needs Fixing (continuous-integration)

Unmerged revisions

136. By Manuel de la Peña

Removed cached password.

135. By Manuel de la Peña

Add u1 icon to action.

134. By Manuel de la Peña

Fixed debian changelog.

133. By Manuel de la Peña

Added payment code to the music store.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configure.ac'
2--- configure.ac 2013-02-15 13:06:28 +0000
3+++ configure.ac 2013-04-17 10:13:28 +0000
4@@ -75,6 +75,7 @@
5 sqlite3 >= 3.7.7
6 gee-1.0
7 json-glib-1.0
8+ libnotify
9 libsoup-2.4
10 oauth
11 unity >= 6.90.0
12
13=== modified file 'debian/changelog'
14--- debian/changelog 2013-03-04 04:01:17 +0000
15+++ debian/changelog 2013-04-17 10:13:28 +0000
16@@ -1,3 +1,11 @@
17+unity-lens-music (6.8.1daily13.03.04-0ubuntu3) UNRELEASED; urgency=low
18+
19+ [ Alejandro J. Cura ]
20+ * debian/control:
21+ - added libnotify-dev build dependency
22+
23+ -- Alejandro J. Cura <alecu@canonical.com> Tue, 19 Mar 2013 18:45:38 -0300
24+
25 unity-lens-music (6.8.1daily13.03.04-0ubuntu1) raring; urgency=low
26
27 [ Pawel Stolowski ]
28
29=== modified file 'debian/control'
30--- debian/control 2012-12-12 16:12:30 +0000
31+++ debian/control 2013-04-17 10:13:28 +0000
32@@ -11,11 +11,12 @@
33 libglib2.0-dev (>= 2.32),
34 libgee-dev,
35 libjson-glib-dev,
36+ libnotify-dev,
37 libsoup2.4-dev,
38 liboauth-dev,
39 libdee-dev (>= 1.0.7),
40 libsqlite3-dev (>= 3.7.7),
41- libunity-dev (>= 6.90.0),
42+ libunity-dev (>= 6.9),
43 libtdb-dev (>= 1.2.6),
44 libgstreamer1.0-dev,
45 libgstreamer-plugins-base1.0-dev
46
47=== modified file 'src/Makefile.am'
48--- src/Makefile.am 2013-02-15 13:06:28 +0000
49+++ src/Makefile.am 2013-04-17 10:13:28 +0000
50@@ -72,8 +72,9 @@
51
52 unity_musicstore_daemon_VALAFLAGS = \
53 --pkg json-glib-1.0 \
54+ --pkg libnotify \
55+ --pkg libsoup-2.4 \
56 --pkg oauth \
57- --pkg libsoup-2.4 \
58 $(unity_music_daemon_VALAFLAGS)
59
60 unity_musicstore_daemon_LDADD = \
61
62=== modified file 'src/album.vala'
63--- src/album.vala 2012-09-11 12:50:34 +0000
64+++ src/album.vala 2013-04-17 10:13:28 +0000
65@@ -28,5 +28,6 @@
66 public string uri { get; set; }
67 public string artwork_path { get; set; }
68 public string formatted_price { get; set; }
69+ public string purchase_sku { get; set; }
70 }
71 }
72\ No newline at end of file
73
74=== modified file 'src/musicstore-collection.vala'
75--- src/musicstore-collection.vala 2012-12-06 19:29:07 +0000
76+++ src/musicstore-collection.vala 2013-04-17 10:13:28 +0000
77@@ -104,6 +104,18 @@
78 track.duration = (int)root_obj.get_member ("duration").get_int ();
79 tracks.append (track);
80 }
81+
82+ if (root_obj.has_member ("id"))
83+ {
84+ album.purchase_sku = root_obj.get_string_member ("id");
85+ }
86+ else
87+ {
88+ // U1MS does not allow purchases of an album with no purchase_sku
89+ album.purchase_sku = "";
90+ warning ("Json has no purchase_sku in '%s'", details_uri);
91+ }
92+
93 }
94 else
95 {
96@@ -125,6 +137,7 @@
97 {
98 var timer = new Timer ();
99 debug ("Searching %s", file.get_uri ());
100+ var empty_asv = new Variant.array (VariantType.VARDICT.element (), {});
101
102 try {
103 var stream = yield file.read_async (Priority.DEFAULT, cancellable);
104
105=== modified file 'src/musicstore-daemon.vala'
106--- src/musicstore-daemon.vala 2012-09-04 09:53:02 +0000
107+++ src/musicstore-daemon.vala 2013-04-17 10:13:28 +0000
108@@ -25,6 +25,8 @@
109 static Application? app = null;
110 static MusicStoreScopeProxy? daemon = null;
111
112+ static const string BUS_NAME = "com.canonical.Unity.Scope.MusicStore";
113+
114 /* Check if a given well known DBus is owned.
115 * WARNING: This does sync IO! */
116 public static bool dbus_name_has_owner (string name)
117@@ -65,7 +67,7 @@
118 * making it race against GDBus' worker thread to export our
119 * objects on the bus before/after owning our name and receiving
120 * method calls on our objects (which may not yet be up!)*/
121- if (dbus_name_has_owner ("com.canonical.Unity.Scope.MusicStore"))
122+ if (dbus_name_has_owner (BUS_NAME))
123 {
124 print ("Another instance of the UbuntuOne Music Daemon " +
125 "already appears to be running.\nBailing out.\n");
126@@ -75,10 +77,14 @@
127 /* Now register our DBus objects *before* acquiring the name!
128 * See above for reasons */
129 daemon = new MusicStoreScopeProxy ();
130+ try {
131+ daemon.scope.export ();
132+ } catch (GLib.IOError e) {
133+ stdout.printf ("error %s\n", e.message);
134+ }
135
136 /* Use GApplication directly for single instance app functionality */
137- app = new Application ("com.canonical.Unity.Scope.MusicStore",
138- ApplicationFlags.IS_SERVICE);
139+ app = new Application (BUS_NAME, ApplicationFlags.IS_SERVICE);
140 try {
141 app.register ();
142 } catch (Error e) {
143
144=== modified file 'src/musicstore-scope.vala'
145--- src/musicstore-scope.vala 2013-02-28 09:48:46 +0000
146+++ src/musicstore-scope.vala 2013-04-17 10:13:28 +0000
147@@ -18,15 +18,30 @@
148 */
149
150 using GLib;
151+using Notify;
152+using Unity;
153+using Gdk;
154+using Ubuntuone.Webservice;
155
156 namespace Unity.MusicLens {
157+ private const string FORGOTTEN_PASSWORD_URL = "https://login.ubuntu.com/+forgot_password";
158+ private const string CHANGE_PAYMENT_METHOD_URL = "https://pay.ubuntu.com/account/";
159+ private const string ERROR_MESSAGE_NOT_LOGGED_IN = _("It seems you don't have an Ubuntu One account, or you are not logged in. To continue, please login and visit the Ubuntu One online store.");
160+ private const string ERROR_MESSAGE_NO_PAYMENT_METHOD = _("It seems you haven't set yet your preferred Ubuntu One payment method. To add a payment method, please visit the Ubuntu One online store.");
161+ private const string ERROR_MESSAGE_TECHNICAL_PROBLEM = _("Sorry, we have encountered a technical problem. No money has been taken from your account. To try your purchase again, please visit the Ubuntu One online store.");
162+ private const string U1_SSO_UI = "u1_login_register";
163
164 public class MusicStoreScopeProxy : SimpleScope
165 {
166+ const string NO_CREDENTIALS_LABEL_TEXT = _("Before you can purchase music you need to log in to the Ubuntu One app.");
167 private MusicStoreCollection collection;
168 private Unity.Extras.PreviewPlayerController preview_player;
169 private Unity.MusicPreview? music_preview;
170 private PreferencesManager preferences = PreferencesManager.get_default ();
171+ private HashTable<string, Album> album_map;
172+ private PurchaseService purchase_service;
173+ private Notification notification;
174+ private bool have_credentials = false;
175
176 public MusicStoreScopeProxy ()
177 {
178@@ -40,7 +55,11 @@
179
180 base.initialize ();
181
182+ Notify.init ("Music Store Scope");
183+ notification = new Notification("Album name", _("Purchase started"), "");
184 collection = new MusicStoreCollection ();
185+ album_map = new HashTable<string, Album>(str_hash, str_equal);
186+ purchase_service = new PurchaseService ();
187
188 preferences.notify["remote-content-search"].connect((obj, pspec) => { scope.queue_search_changed(SearchType.DEFAULT); });
189
190@@ -65,11 +84,11 @@
191 else
192 {
193 warning ("Failed to generate preview for %s", uri);
194- return download_album (uri);
195+ return open_uri (uri);
196 }
197 }
198
199- public Unity.ActivationResponse download_album (string uri)
200+ public Unity.ActivationResponse open_uri (string uri)
201 {
202 /* launch the music store streaming client or the webstore or whatevz */
203 try {
204@@ -89,6 +108,7 @@
205
206 if (album != null)
207 {
208+ album_map.insert (uri, album);
209 File cover_file = File.new_for_uri (album.artwork_path); //artwork path is a remote uri
210 var cover = new FileIcon (cover_file);
211
212@@ -111,12 +131,19 @@
213 }
214 }
215
216- GLib.Icon? icon = new GLib.FileIcon (File.new_for_path (Config.DATADIR + "/icons/unity-icon-theme/places/svg/service-u1.svg"));
217- var download_action = new Unity.PreviewAction ("download_album", _("Download"), icon);
218- if (album.formatted_price != null)
219- download_action.extra_text = album.formatted_price;
220- download_action.activated.connect (download_album);
221- music_preview.add_action (download_action);
222+ if (have_credentials) {
223+ GLib.Icon? icon = new GLib.FileIcon (File.new_for_path (Config.DATADIR + "/icons/unity-icon-theme/places/svg/service-u1.svg"));
224+ var download_action = new Unity.PreviewAction ("show_purchase_preview", _("Download"), icon);
225+ if (album.formatted_price != null)
226+ download_action.extra_text = album.formatted_price;
227+ download_action.activated.connect (show_purchase_preview);
228+ music_preview.add_action (download_action);
229+ } else {
230+ var data = new HashTable<string, Variant>(str_hash, str_equal);
231+ data["no_credentials_label"] = NO_CREDENTIALS_LABEL_TEXT;
232+ InfoHint info_hint = new InfoHint.with_variant("music_preview", "", null, data);
233+ music_preview.add_info(info_hint);
234+ }
235 }
236 return music_preview;
237 }
238@@ -142,12 +169,227 @@
239 }
240
241 try {
242- debug ("model has %u rows before search", search.results_model.get_n_rows ());
243- yield collection.search (search, search_type, (owned) filters, max_results, cancellable);
244- debug ("model has %u rows after search", search.results_model.get_n_rows ());
245+ yield purchase_service.fetch_credentials ();
246+ have_credentials = true;
247+ } catch (PurchaseError e) {
248+ // this is not a serious error, just missing credentials
249+ have_credentials = false;
250+ }
251+
252+ try {
253+ debug ("model has %u rows before search", search.results_model.get_n_rows ());
254+ yield collection.search (search, search_type, (owned) filters, max_results, cancellable);
255+ debug ("model has %u rows after search", search.results_model.get_n_rows ());
256 } catch (IOError e) {
257- warning ("Failed to search for '%s': %s", search.search_string, e.message);
258- }
259+ warning ("Failed to search for '%s': %s", search.search_string, e.message);
260+ }
261+
262+ try {
263+ yield purchase_service.fetch_account_info ();
264+ debug ("retrieved account info: %s %s", purchase_service.nickname, purchase_service.email);
265+ } catch (PurchaseError e) {
266+ debug ("can't get account info: %s", e.message);
267+ }
268+ }
269+
270+ delegate Unity.ActivationResponse LinkHandler (string uri);
271+
272+ private Unity.ActivationResponse build_error_preview (string uri, string error_header, string link_text, LinkHandler link_handler)
273+ {
274+ Album album = null;
275+ SList<Track> tracks = null;
276+ collection.get_album_details (uri, out album, out tracks);
277+ debug ("album art uri: %s", album.artwork_path);
278+ File cover_file = File.new_for_uri (album.artwork_path); //artwork path is a remote uri
279+ var cover = new FileIcon (cover_file);
280+ var error_preview = new Unity.PaymentPreview.for_error(album.title, album.artist, cover);
281+ error_preview.header = error_header;
282+
283+ var cancel_action = new Unity.PreviewAction ("cancel", _("Cancel"), null);
284+ cancel_action.activated.connect (() => cancel_purchase (uri));
285+ error_preview.add_action (cancel_action);
286+
287+ GLib.Icon? icon = new GLib.FileIcon (File.new_for_path (Config.DATADIR + "/icons/unity-icon-theme/places/svg/service-u1.svg"));
288+ var error_action = new Unity.PreviewAction ("go_to_u1", link_text, icon);
289+ error_action.activated.connect (() => link_handler (uri));
290+ error_preview.add_action (error_action);
291+ return new Unity.ActivationResponse.with_preview (error_preview);
292+ }
293+
294+ public Unity.ActivationResponse open_forgot_password_url (string uri)
295+ {
296+ return open_uri (FORGOTTEN_PASSWORD_URL);
297+ }
298+
299+ public Unity.ActivationResponse change_payment_method (string uri)
300+ {
301+ if (purchase_service.open_url != null)
302+ {
303+ debug ("Open url is %s", purchase_service.open_url);
304+ return open_uri (purchase_service.open_url);
305+ }
306+ return open_uri (CHANGE_PAYMENT_METHOD_URL);
307+ }
308+
309+ public Unity.ActivationResponse cancel_purchase (string uri)
310+ {
311+ return new Unity.ActivationResponse.with_preview (preview (uri));
312+ }
313+
314+ public Unity.ActivationResponse open_sso_login (string uri)
315+ {
316+ Album album = null;
317+ SList<Track> tracks = null;
318+ collection.get_album_details (uri, out album, out tracks);
319+
320+ var ui_path = Environment.find_program_in_path (U1_SSO_UI);
321+ GLib.Pid child_pid;
322+ string argv[11] = {
323+ ui_path,
324+ "--album", album.title,
325+ "--artist", album.artist,
326+ "--price", album.formatted_price,
327+ "--picture", album.artwork_path,
328+ "--url", uri
329+ };
330+ if (ui_path != null) {
331+ try {
332+ bool was_started = Process.spawn_async (null, argv, null, 0, null, out child_pid);
333+ // hide dash
334+ if (was_started) {
335+ return new Unity.ActivationResponse (Unity.HandledType.HIDE_DASH);
336+ }
337+ } catch (GLib.SpawnError e) {
338+ debug ("Failed to start u1_sso_ui for uri %s", uri);
339+ }
340+ }
341+ return open_uri (uri);
342+ }
343+
344+ public Unity.ActivationResponse show_purchase_preview (string uri)
345+ {
346+ if (purchase_service.got_credentials () == false)
347+ {
348+ debug ("no credentials available, opening sso login. %s", uri);
349+ return build_error_preview (uri, ERROR_MESSAGE_NOT_LOGGED_IN, _("Continue"), open_sso_login);
350+ }
351+
352+ Album album = null;
353+ SList<Track> tracks = null;
354+ collection.get_album_details (uri, out album, out tracks);
355+ if (album != null)
356+ {
357+ try {
358+ purchase_service.fetch_payment_info (album.purchase_sku);
359+ debug ("retrieved payment method: %s", purchase_service.selected_payment_method);
360+ return new Unity.ActivationResponse.with_preview (purchase_preview (uri, null));
361+ } catch (PurchaseError e) {
362+ debug ("can't get default payment method: %s", e.message);
363+ return build_error_preview (uri, ERROR_MESSAGE_NO_PAYMENT_METHOD, _("Choose Payment Method"), change_payment_method);
364+ }
365+ }
366+ return open_uri (uri);
367+ }
368+
369+ public Unity.ActivationResponse purchase_album (Unity.PreviewAction action, string uri)
370+ {
371+ var password = action.hints["password"].get_string();
372+
373+ if (password == null) {
374+ var preview = purchase_preview (uri, _("Please enter your password"));
375+ debug ("empty password.");
376+ return new Unity.ActivationResponse.with_preview (preview);
377+ }
378+
379+ var album = album_map.get (uri);
380+
381+ File cover_file = File.new_for_uri (album.artwork_path); //artwork path is a remote uri
382+ try {
383+ var cover_pixbuf = new Pixbuf.from_stream (cover_file.read ());
384+ notification.set_icon_from_pixbuf (cover_pixbuf);
385+ } catch (GLib.Error e) {
386+ debug ("Cannot set notification icon from uri %s", uri);
387+ }
388+
389+ notification.summary = album.title;
390+ notification.body = _("Authorizing purchase");
391+ try {
392+ notification.show ();
393+ } catch (GLib.Error e) {
394+ debug ("Error while showing notification: %s", e.message);
395+ }
396+
397+ try {
398+ purchase_service.purchase (album.purchase_sku, password);
399+ debug ("purchase completed.");
400+ notification.update (album.title, _("Purchase completed"), "");
401+ try {
402+ notification.show ();
403+ } catch (GLib.Error e) {
404+ debug ("Error while showing notification: %s", e.message);
405+ }
406+ return new Unity.ActivationResponse (Unity.HandledType.HIDE_DASH);
407+ } catch (PurchaseError e) {
408+ if (e is PurchaseError.WRONG_PASSWORD_ERROR) {
409+ debug ("wrong password error: %s", e.message);
410+ return new Unity.ActivationResponse.with_preview (purchase_preview (uri, _("Wrong password")));
411+ } else {
412+ debug ("got purchase error: %s", e.message);
413+ return build_error_preview (uri, ERROR_MESSAGE_TECHNICAL_PROBLEM, _("Continue"), open_uri);
414+ }
415+ }
416+ }
417+
418+ public Unity.Preview purchase_preview (string uri, string? error_message)
419+ {
420+ Unity.PaymentPreview album_purchase_preview = null;
421+ Album album = null;
422+ SList<Track> tracks = null;
423+ collection.get_album_details (uri, out album, out tracks);
424+
425+ if (album != null)
426+ {
427+ File cover_file = File.new_for_uri (album.artwork_path); //artwork path is a remote uri
428+
429+ var cover = new FileIcon (cover_file);
430+ album_purchase_preview = new Unity.PaymentPreview.for_music(album.title, album.artist, cover);
431+
432+ album_purchase_preview.header = _("Hi %s, you purchased in the past from Ubuntu One,"
433+ + " would you like to use the same payment details? Please review your order.").printf (purchase_service.nickname);
434+ album_purchase_preview.email = purchase_service.email;
435+ album_purchase_preview.payment_method = purchase_service.selected_payment_method;
436+ album_purchase_preview.purchase_prize = album.formatted_price;
437+ album_purchase_preview.purchase_type = _("Digital CD");
438+
439+ // data
440+
441+ var data = new HashTable<string, Variant>(str_hash, str_equal);
442+ if (error_message != null) {
443+ data["error_message"] = error_message;
444+ }
445+
446+ InfoHint info_hint = new InfoHint.with_variant("album_purchase_preview", "", null, data);
447+ album_purchase_preview.add_info(info_hint);
448+
449+ // actions
450+
451+ var purchase_action = new Unity.PreviewAction ("purchase_album", _("Purchase"), null);
452+ purchase_action.activated.connect (purchase_album);
453+ album_purchase_preview.add_action (purchase_action);
454+
455+ var forgot_password_action = new Unity.PreviewAction ("forgot_password", _("forgotten your Ubuntu One password?"), null);
456+ forgot_password_action.activated.connect (open_forgot_password_url);
457+ album_purchase_preview.add_action (forgot_password_action);
458+
459+ var cancel_action = new Unity.PreviewAction ("cancel_purchase", _("Cancel"), null);
460+ cancel_action.activated.connect (cancel_purchase);
461+ album_purchase_preview.add_action (cancel_action);
462+
463+ var change_payment_method_action = new Unity.PreviewAction ("change_payment_method", _("change payment method"), null);
464+ change_payment_method_action.activated.connect (change_payment_method);
465+ album_purchase_preview.add_action (change_payment_method_action);
466+ }
467+ return album_purchase_preview;
468 }
469
470 }
471
472=== modified file 'src/ubuntuone-webservices.vala'
473--- src/ubuntuone-webservices.vala 2012-12-06 15:39:38 +0000
474+++ src/ubuntuone-webservices.vala 2013-04-17 10:13:28 +0000
475@@ -21,26 +21,29 @@
476
477 [DBus (name = "com.ubuntuone.CredentialsManagement")]
478 interface CredentialsManagement : GLib.Object {
479+ public signal void authorization_denied ();
480 public signal void credentials_found (HashTable <string, string> info);
481+ public signal void credentials_not_found ();
482 public signal void credentials_error ();
483
484 [DBus (name = "find_credentials")]
485 public abstract void find_credentials () throws IOError;
486 }
487
488-const string WEBAPI_URI = "https://edge.one.ubuntu.com/";
489-const string ACCOUNT_URI = WEBAPI_URI + "api/account";
490-const string PAYMENT_METHOD_URI = WEBAPI_URI + "music-store/api/1/user/retrieve-payment-method?purchase_sku=%s";
491-const string PURCHASE_WITH_DEFAULT_PAYMENT_URI = WEBAPI_URI + "music-store/api/1/user/purchase-with-default-payment?purchase_sku=%s&authentication=%s";
492-const string AUTHENTICATION_URI = "https://login.staging.ubuntu.com/api/1.1/authentications";
493+const string WEBAPI_SERVER = "https://one.ubuntu.com/";
494+const string ACCOUNT_PATH = "api/account";
495+const string PAYMENT_METHOD_PATH = "music-store-up/api/1/user/retrieve-payment-method?purchase_sku=%s";
496+const string PURCHASE_WITH_DEFAULT_PAYMENT_PATH = "music-store-up/api/1/user/purchase-with-default-payment?purchase_sku=%s&authentication=%s";
497+const string AUTHENTICATION_SERVER = "https://login.ubuntu.com/";
498+const string AUTHENTICATION_PATH = "api/1.1/authentications";
499 const string AUTHENTICATE_PARAMS = "ws.op=authenticate&token_name=Purchase_Token";
500-const int PASSWORD_CACHE_SECONDS = 300;
501-const int PAYMENT_METHOD_CACHE_SECONDS = 120;
502+
503
504 namespace Ubuntuone.Webservice
505 {
506 public errordomain PurchaseError
507 {
508+ MISSING_CREDENTIALS_ERROR,
509 PURCHASE_ERROR,
510 WRONG_PASSWORD_ERROR,
511 UNSPECIFIED_ERROR
512@@ -56,17 +59,13 @@
513 public string selected_payment_method { get; internal set; default = null; }
514 public string consumer_key { get; private set; default = null; }
515 public string token { get; private set; default = null; }
516- internal DateTime _selected_payment_method_expiry;
517- DateTime _password_expiry;
518+ public string open_url { get; private set; default = null; }
519 internal HashTable <string, string> _ubuntuone_credentials = null;
520
521 construct {
522 http_session = build_http_session ();
523 http_session_sso = build_http_session ();
524
525- reset_payment_method_cache (0);
526- reset_password_cache (0);
527-
528 credentials_management = build_credentials_management ();
529 }
530
531@@ -77,24 +76,40 @@
532 return session;
533 }
534
535- internal DateTime now ()
536- {
537- return new DateTime.now_utc ();
538- }
539-
540- internal bool expired_payment_method_cache ()
541- {
542- return _selected_payment_method_expiry.compare (now ()) <= 0;
543- }
544-
545- internal void reset_payment_method_cache (int seconds_from_now)
546- {
547- _selected_payment_method_expiry = now ().add_seconds (seconds_from_now);
548- }
549-
550- internal void reset_password_cache (int seconds_from_now)
551- {
552- _password_expiry = now ().add_seconds (seconds_from_now);
553+ string webapi_server ()
554+ {
555+ string staging_webapi = Environment.get_variable ("U1_STAGING_WEBAPI");
556+ return staging_webapi != null ? staging_webapi : WEBAPI_SERVER;
557+ }
558+
559+ string account_uri ()
560+ {
561+ return webapi_server() + ACCOUNT_PATH;
562+ }
563+
564+ string payment_method_uri ()
565+ {
566+ return webapi_server() + PAYMENT_METHOD_PATH;
567+ }
568+
569+ string purchase_with_default_payment_uri ()
570+ {
571+ return webapi_server() + PURCHASE_WITH_DEFAULT_PAYMENT_PATH;
572+ }
573+
574+ string authentication_server ()
575+ {
576+ string staging_authentication = Environment.get_variable ("U1_STAGING_AUTHENTICATION");
577+ return staging_authentication != null ? staging_authentication : AUTHENTICATION_SERVER;
578+ }
579+
580+ string authentication_uri ()
581+ {
582+ return authentication_server() + AUTHENTICATION_PATH;
583+ }
584+
585+ public bool got_credentials () {
586+ return _ubuntuone_credentials != null;
587 }
588
589 internal virtual CredentialsManagement build_credentials_management ()
590@@ -125,28 +140,36 @@
591 email = root_object.get_string_member("email");
592 }
593
594- internal void parse_payment_method_json (string json_string) throws GLib.Error
595+ internal void parse_payment_method_json (string json_string) throws GLib.Error, PurchaseError
596 {
597 var root_object = parse_json (json_string);
598- Json.Object payload = root_object.get_object_member("payload");
599- selected_payment_method = payload.get_string_member("selected_payment_method");
600+ if (root_object.has_member ("selected_payment_method")) {
601+ selected_payment_method = root_object.get_string_member("selected_payment_method");
602+ } else {
603+ open_url = root_object.get_string_member ("open_url");
604+ var error_message = root_object.get_string_member ("error_message");
605+ throw new PurchaseError.PURCHASE_ERROR (error_message);
606+ }
607 }
608
609 internal void parse_authentication_json (string json_string) throws GLib.Error
610 {
611 var root_object = parse_json (json_string);
612- consumer_key = root_object.get_string_member("consumer_key");
613- token = root_object.get_string_member("token");
614+ consumer_key = root_object.get_string_member ("consumer_key");
615+ token = root_object.get_string_member ("token");
616 }
617
618 internal string parse_purchase_json (string json_string) throws GLib.Error
619 {
620 var root_object = parse_json (json_string);
621- Json.Object payload = root_object.get_object_member("payload");
622- return payload.get_string_member("open_url");
623+ if (root_object.has_member ("open_url")) {
624+ return root_object.get_string_member("open_url");
625+ } else {
626+ return "";
627+ }
628 }
629
630- internal virtual async void fetch_credentials () throws PurchaseError
631+ public virtual async void fetch_credentials () throws PurchaseError
632 {
633 PurchaseError error = null;
634
635@@ -155,8 +178,12 @@
636 debug ("got credentials");
637 fetch_credentials.callback ();
638 });
639+ ulong not_found_handler = credentials_management.credentials_not_found.connect ((credentials) => {
640+ error = new PurchaseError.MISSING_CREDENTIALS_ERROR ("Can't get Ubuntu One tokens.");
641+ fetch_credentials.callback ();
642+ });
643 ulong error_handler = credentials_management.credentials_error.connect (() => {
644- error = new PurchaseError.PURCHASE_ERROR ("Can't get Ubuntu One tokens.");
645+ error = new PurchaseError.MISSING_CREDENTIALS_ERROR ("Can't get Ubuntu One tokens.");
646 fetch_credentials.callback ();
647 });
648
649@@ -164,10 +191,11 @@
650 credentials_management.find_credentials ();
651 yield;
652 } catch (IOError e) {
653- error = new PurchaseError.PURCHASE_ERROR ("Can't get Ubuntu One tokens: %s", e.message);
654+ error = new PurchaseError.MISSING_CREDENTIALS_ERROR ("Can't get Ubuntu One tokens: %s", e.message);
655 }
656
657 credentials_management.disconnect (found_handler);
658+ credentials_management.disconnect (not_found_handler);
659 credentials_management.disconnect (error_handler);
660
661 if (error != null) {
662@@ -208,7 +236,7 @@
663 internal virtual async void fetch_account () throws PurchaseError
664 {
665 string response;
666- PurchaseError error = yield call_api ("GET", ACCOUNT_URI, out response);
667+ PurchaseError error = yield call_api ("GET", account_uri(), out response);
668
669 if (error != null) {
670 debug ("Error while fetching U1 account: %s.", error.message);
671@@ -224,43 +252,40 @@
672 }
673 }
674
675- internal virtual async void fetch_payment_method (string purchase_sku) throws PurchaseError
676+ internal virtual void fetch_payment_method (string purchase_sku) throws PurchaseError
677 {
678- string response;
679- var uri = PAYMENT_METHOD_URI.printf (purchase_sku);
680- PurchaseError error = yield call_api ("GET", uri, out response);
681+ var uri = payment_method_uri().printf (purchase_sku);
682
683- if (error != null) {
684- debug ("Error while fetching payment method: %s.", error.message);
685- throw error;
686+ var message = send_signed_webservice_call ("GET", uri);
687+ if (message.status_code != Soup.KnownStatusCode.OK) {
688+ debug ("Purchase request failed: HTTP %u", message.status_code);
689+ debug ("Reason: %s", message.reason_phrase);
690+ try {
691+ message.response_body.flatten ();
692+ debug ("body: ------\n%s\n------\n", (string) message.response_body.data);
693+ } catch (Error e) {
694+ }
695+ throw new PurchaseError.PURCHASE_ERROR ("Retrieve payment method failed: %s".printf (message.reason_phrase));
696 }
697-
698 try {
699- parse_payment_method_json (response);
700- debug ("got payment method");
701+ message.response_body.flatten ();
702+ var result = (string) message.response_body.data;
703+ parse_payment_method_json (result);
704 } catch (GLib.Error e) {
705 debug ("Error while getting payment method: %s.", e.message);
706 throw new PurchaseError.PURCHASE_ERROR (e.message);
707 }
708 }
709
710- internal virtual async void refetch_payment_info (string purchase_sku) throws PurchaseError
711+ public virtual async void fetch_account_info () throws PurchaseError
712 {
713 yield fetch_credentials ();
714 yield fetch_account ();
715- yield fetch_payment_method (purchase_sku);
716- reset_payment_method_cache (PAYMENT_METHOD_CACHE_SECONDS);
717 }
718
719- public async void fetch_payment_info (string purchase_sku) throws PurchaseError
720+ public void fetch_payment_info (string purchase_sku) throws PurchaseError
721 {
722- if (expired_payment_method_cache ())
723- {
724- debug ("refetching");
725- yield refetch_payment_info (purchase_sku);
726- } else {
727- debug ("in cache");
728- }
729+ fetch_payment_method (purchase_sku);
730 }
731
732 internal virtual void _do_sso_webcall (Soup.Message message, string password)
733@@ -286,6 +311,11 @@
734 if (message.status_code == Soup.KnownStatusCode.UNAUTHORIZED) {
735 throw new PurchaseError.WRONG_PASSWORD_ERROR ("Wrong password");
736 }
737+ try {
738+ message.response_body.flatten ();
739+ debug ("body: ------\n%s\n------\n", (string) message.response_body.data);
740+ } catch (Error e) {
741+ }
742 throw new PurchaseError.PURCHASE_ERROR (message.reason_phrase);
743 }
744 message.response_body.flatten ();
745@@ -294,12 +324,13 @@
746
747 internal virtual string get_purchase_token (string password) throws PurchaseError
748 {
749- var result = authenticated_sso_webcall ("POST", AUTHENTICATION_URI, AUTHENTICATE_PARAMS, password);
750+ var result = authenticated_sso_webcall ("POST", authentication_uri(), AUTHENTICATE_PARAMS, password);
751 try {
752 parse_authentication_json (result);
753 } catch (GLib.Error e) {
754 throw new PurchaseError.PURCHASE_ERROR (e.message);
755 }
756+
757 return "%s:%s".printf (consumer_key, token);
758 }
759
760@@ -311,14 +342,20 @@
761 return message;
762 }
763
764- internal virtual void purchase_with_default_payment (string sku, string purchase_token) throws PurchaseError
765+ internal virtual void purchase_with_default_payment (string album_id, string purchase_token) throws PurchaseError
766 {
767- var uri = PURCHASE_WITH_DEFAULT_PAYMENT_URI.printf (sku, purchase_token);
768+ var uri = purchase_with_default_payment_uri().printf (album_id, purchase_token);
769+
770 var message = send_signed_webservice_call ("GET", uri);
771
772 if (message.status_code != Soup.KnownStatusCode.OK) {
773 debug ("Purchase request failed: HTTP %u", message.status_code);
774 debug ("Reason: %s", message.reason_phrase);
775+ try {
776+ message.response_body.flatten ();
777+ debug ("body: ------\n%s\n------\n", (string) message.response_body.data);
778+ } catch (Error e) {
779+ }
780 throw new PurchaseError.PURCHASE_ERROR ("Purchase failed: %s".printf (message.reason_phrase));
781 }
782 try {
783@@ -336,7 +373,9 @@
784 public void purchase (string album_id, string password) throws PurchaseError
785 {
786 var purchase_token = get_purchase_token (password);
787+ debug ("purchasing...");
788 purchase_with_default_payment (album_id, purchase_token);
789+ debug ("purchase completed.");
790 }
791 }
792 }
793
794=== modified file 'tests/unit/test-ubuntuone-purchases.vala'
795--- tests/unit/test-ubuntuone-purchases.vala 2012-12-06 15:39:38 +0000
796+++ tests/unit/test-ubuntuone-purchases.vala 2013-04-17 10:13:28 +0000
797@@ -25,7 +25,7 @@
798
799 const string FAKE_URL = "http://fake/url";
800 const string FAKE_PASSWORD = "PezEspada";
801-const string FAKE_SKU = "fake_store:fake_album:id";
802+const string FAKE_SKU = "7digital:fake_album:id";
803 const string FAKE_TOKEN = "a fake token";
804
805 const string BROKEN_JSON = """
806@@ -45,14 +45,17 @@
807
808 const string FAKE_JSON_PAYMENT_METHOD = """
809 {
810- "meta": {
811- "status": "Ok"
812- },
813- "payload": {
814- "open_url": "",
815- "user_email": "mr@be.an",
816- "selected_payment_method": "Visa 1234"
817- }
818+ "open_url": "",
819+ "user_email": "mr@be.an",
820+ "selected_payment_method": "Visa 1234"
821+ }
822+""";
823+
824+
825+const string FAKE_JSON_PAYMENT_METHOD_ERROR = """
826+ {
827+ "open_url": "http://somewhere/else",
828+ "error_message": "No default payment method selected."
829 }
830 """;
831
832@@ -69,30 +72,18 @@
833
834 const string FAKE_JSON_PURCHASE = """
835 {
836- "meta": {
837- "status": "Ok"
838- },
839- "payload": {
840- "order_id": "1111",
841- "order_status": "OK",
842- "order_detail": "TBD",
843- "open_url": ""
844- }
845+ "order_id": "1111",
846+ "order_status": "OK"
847 }
848 """;
849
850
851 const string FAKE_JSON_PURCHASE_FAILURE = """
852 {
853- "meta": {
854- "status": "Ok"
855- },
856- "payload": {
857- "order_id": "1111",
858- "order_status": "PAYMENT_FAILURE",
859- "order_detail": "TBD",
860- "open_url": "http://slashdot.org/"
861- }
862+ "order_id": "1111",
863+ "order_status": "PAYMENT_FAILURE",
864+ "order_detail": "TBD",
865+ "open_url": "http://slashdot.org/"
866 }
867 """;
868
869@@ -113,12 +104,11 @@
870 Test.add_data_func ("/Unit/PurchaseChecker/ParseAccount", test_parse_account);
871 Test.add_data_func ("/Unit/PurchaseChecker/ParseBrokenAccount", test_parse_broken_account);
872 Test.add_data_func ("/Unit/PurchaseChecker/ParsePaymentMethod", test_parse_payment_method);
873+ Test.add_data_func ("/Unit/PurchaseChecker/ParsePaymentMethodError", test_parse_payment_method_error);
874 Test.add_data_func ("/Unit/PurchaseChecker/ParseAuthentication", test_parse_authentication);
875 Test.add_data_func ("/Unit/PurchaseChecker/ReadyToPurchase", test_ready_to_purchase);
876- Test.add_data_func ("/Unit/PurchaseChecker/FetchPaymentInvalidated", test_fetch_payment_invalidated);
877- Test.add_data_func ("/Unit/PurchaseChecker/FetchPaymentCached", test_fetch_payment_cached);
878- Test.add_data_func ("/Unit/PurchaseChecker/ReFetchPayment", test_refetch_payment);
879- Test.add_data_func ("/Unit/PurchaseChecker/ReFetchPaymentSetsCache", test_refetch_payment_sets_cache);
880+ Test.add_data_func ("/Unit/PurchaseChecker/FetchAccountInfo", test_fetch_account_info);
881+ Test.add_data_func ("/Unit/PurchaseChecker/FetchPaymentInfo", test_fetch_payment_info);
882 Test.add_data_func ("/Unit/PurchaseChecker/FetchCredentials", test_fetch_credentials);
883 Test.add_data_func ("/Unit/PurchaseChecker/FetchCredentialsFails", test_fetch_credentials_fails);
884 Test.add_data_func ("/Unit/PurchaseChecker/FetchCredentialsFailsExtra", test_fetch_credentials_fails_extra);
885@@ -163,11 +153,12 @@
886 async void irl_test_async ()
887 {
888 var purchase_service = new PurchaseService();
889- var purchase_sku = "1";
890+ var purchase_sku = "7digital:album:1347423:WORLD";
891 try {
892- yield purchase_service.fetch_payment_info (purchase_sku);
893+ yield purchase_service.fetch_account_info ();
894+ purchase_service.fetch_payment_info (purchase_sku);
895 debug ("data was available: %s %s %s", purchase_service.nickname, purchase_service.email, purchase_service.selected_payment_method);
896- var real_user_password = "****fake_password_in_bzr***";
897+ var real_user_password = FAKE_PASSWORD;
898 purchase_service.purchase (purchase_sku, real_user_password);
899 debug ("purchase completed.");
900 } catch (PurchaseError e) {
901@@ -186,7 +177,7 @@
902 });
903 return false;
904 });
905- assert (run_with_timeout (loop, 10000));
906+ assert (run_with_timeout (loop, 60000));
907 }
908
909 class FakeCredentialsManagement : GLib.Object, CredentialsManagement
910@@ -290,6 +281,19 @@
911 assert_cmpstr (purchase_service.selected_payment_method, OperatorType.EQUAL, "Visa 1234");
912 }
913
914+ private static void test_parse_payment_method_error ()
915+ {
916+ var purchase_service = new BaseTestPurchaseService ();
917+ try
918+ {
919+ purchase_service.parse_payment_method_json (FAKE_JSON_PAYMENT_METHOD_ERROR);
920+ assert_not_reached ();
921+ } catch (PurchaseError e)
922+ {
923+ assert_cmpstr (purchase_service.open_url, OperatorType.EQUAL, "http://somewhere/else");
924+ }
925+ }
926+
927 private static void test_parse_authentication ()
928 {
929 var purchase_service = new BaseTestPurchaseService ();
930@@ -313,30 +317,6 @@
931 assert (purchase_service.ready_to_purchase == true);
932 }
933
934- class TestFetchPurchaseService : BaseTestPurchaseService {
935- internal bool fetch_called = false;
936- internal override async void refetch_payment_info (string purchase_sku)
937- {
938- fetch_called = true;
939- }
940- }
941-
942- private static void test_fetch_payment_invalidated ()
943- {
944- var purchase_service = new TestFetchPurchaseService ();
945- purchase_service._selected_payment_method_expiry = new DateTime.now_utc ();
946- purchase_service.fetch_payment_info ("fake_sku");
947- assert (purchase_service.fetch_called);
948- }
949-
950- private static void test_fetch_payment_cached ()
951- {
952- var purchase_service = new TestFetchPurchaseService ();
953- purchase_service._selected_payment_method_expiry = new DateTime.now_utc ().add_seconds (60);
954- purchase_service.fetch_payment_info ("fake_sku");
955- assert (purchase_service.fetch_called == false);
956- }
957-
958 class TestRefetchPurchaseService : BaseTestPurchaseService {
959 internal bool credentials_fetched = false;
960 internal bool account_fetched = false;
961@@ -360,26 +340,21 @@
962 });
963 yield;
964 }
965- internal override async void fetch_payment_method (string purchase_sku)
966+ internal override void fetch_payment_method (string purchase_sku)
967 {
968- Idle.add (() => {
969- payment_method_fetched = true;
970- fetch_payment_method.callback ();
971- return false;
972- });
973- yield;
974+ payment_method_fetched = true;
975 }
976 }
977
978- private static void test_refetch_payment ()
979+ private static void test_fetch_account_info ()
980 {
981 var purchase_service = new TestRefetchPurchaseService ();
982
983 MainLoop mainloop = new MainLoop ();
984- purchase_service.refetch_payment_info.begin("fake_sku", (obj, res) => {
985+ purchase_service.fetch_account_info.begin((obj, res) => {
986 mainloop.quit ();
987 try {
988- purchase_service.refetch_payment_info.end (res);
989+ purchase_service.fetch_account_info.end (res);
990 } catch (PurchaseError e) {
991 error ("Can't fetch payment info: %s", e.message);
992 }
993@@ -388,29 +363,19 @@
994
995 assert (purchase_service.credentials_fetched);
996 assert (purchase_service.account_fetched);
997+ }
998+
999+ private static void test_fetch_payment_info ()
1000+ {
1001+ var purchase_service = new TestRefetchPurchaseService ();
1002+ try {
1003+ purchase_service.fetch_payment_info("fake_sku");
1004+ } catch (PurchaseError e) {
1005+ error ("Can't fetch payment info: %s", e.message);
1006+ }
1007 assert (purchase_service.payment_method_fetched);
1008 }
1009
1010- private static void test_refetch_payment_sets_cache ()
1011- {
1012- var purchase_service = new TestRefetchPurchaseService ();
1013-
1014- assert (true == purchase_service.expired_payment_method_cache ());
1015-
1016- MainLoop mainloop = new MainLoop ();
1017- purchase_service.refetch_payment_info.begin("fake_sku", (obj, res) => {
1018- mainloop.quit ();
1019- try {
1020- purchase_service.refetch_payment_info.end (res);
1021- } catch (PurchaseError e) {
1022- error ("Can't fetch payment info: %s", e.message);
1023- }
1024- });
1025- assert (run_with_timeout (mainloop, 1000));
1026-
1027- assert (false == purchase_service.expired_payment_method_cache ());
1028- }
1029-
1030 private static void test_fetch_credentials ()
1031 {
1032 var purchase_service = new BaseTestPurchaseService ();
1033@@ -431,7 +396,7 @@
1034
1035 private static void test_fetch_credentials_fails ()
1036 {
1037- bool failed = false;
1038+ PurchaseError failure = null;
1039 var purchase_service = new FailingCredentialsPurchaseService ();
1040
1041 MainLoop mainloop = new MainLoop ();
1042@@ -440,17 +405,16 @@
1043 try {
1044 purchase_service.fetch_credentials.end (res);
1045 } catch (PurchaseError e) {
1046- failed = true;
1047+ failure = e;
1048 }
1049 });
1050 assert (run_with_timeout (mainloop, 1000));
1051-
1052- assert (failed);
1053+ assert (failure is PurchaseError.MISSING_CREDENTIALS_ERROR);
1054 }
1055
1056 private static void test_fetch_credentials_fails_extra ()
1057 {
1058- bool failed = false;
1059+ PurchaseError failure = null;
1060 var purchase_service = new ExtraFailingCredentialsPurchaseService ();
1061
1062 MainLoop mainloop = new MainLoop ();
1063@@ -459,12 +423,11 @@
1064 try {
1065 purchase_service.fetch_credentials.end (res);
1066 } catch (PurchaseError e) {
1067- failed = true;
1068+ failure = e;
1069 }
1070 });
1071 assert (run_with_timeout (mainloop, 1000));
1072-
1073- assert (failed);
1074+ assert (failure is PurchaseError.MISSING_CREDENTIALS_ERROR);
1075 }
1076
1077 class FakeWebcallPurchaseService : BaseTestPurchaseService
1078@@ -565,21 +528,7 @@
1079 {
1080 var purchase_service = new FakeWebcallPurchaseService (_status_code, _found_json);
1081 purchase_service._ubuntuone_credentials = new HashTable <string, string> (str_hash, str_equal);
1082-
1083- PurchaseError error = null;
1084- MainLoop mainloop = new MainLoop ();
1085- purchase_service.fetch_payment_method.begin("fake_sku", (obj, res) => {
1086- mainloop.quit ();
1087- try {
1088- purchase_service.fetch_payment_method.end (res);
1089- } catch (PurchaseError e) {
1090- error = e;
1091- }
1092- });
1093- assert (run_with_timeout (mainloop, 1000));
1094- if (error != null) {
1095- throw error;
1096- }
1097+ purchase_service.fetch_payment_method("fake_sku");
1098 return purchase_service;
1099 }
1100
1101@@ -616,7 +565,7 @@
1102 class TestPurchaseService : BaseTestPurchaseService
1103 {
1104 internal string password;
1105- internal string sku;
1106+ internal string album_id;
1107 internal string purchase_token;
1108
1109 internal override string get_purchase_token (string password)
1110@@ -624,9 +573,9 @@
1111 this.password = password;
1112 return FAKE_TOKEN;
1113 }
1114- internal override void purchase_with_default_payment (string sku, string purchase_token)
1115+ internal override void purchase_with_default_payment (string album_id, string purchase_token)
1116 {
1117- this.sku = sku;
1118+ this.album_id = album_id;
1119 this.purchase_token = purchase_token;
1120 }
1121 }
1122@@ -637,7 +586,7 @@
1123 var purchase_service = new TestPurchaseService ();
1124 purchase_service.purchase (FAKE_SKU, FAKE_PASSWORD);
1125 assert_cmpstr (purchase_service.password, OperatorType.EQUAL, FAKE_PASSWORD);
1126- assert_cmpstr (purchase_service.sku, OperatorType.EQUAL, FAKE_SKU);
1127+ assert_cmpstr (purchase_service.album_id, OperatorType.EQUAL, FAKE_SKU);
1128 assert_cmpstr (purchase_service.purchase_token, OperatorType.EQUAL, FAKE_TOKEN);
1129 } catch (Error e) {
1130 assert_not_reached ();
1131@@ -722,6 +671,7 @@
1132 try {
1133 purchase_service.purchase_with_default_payment (FAKE_SKU, FAKE_TOKEN);
1134 } catch (PurchaseError e) {
1135+ warning (e.message);
1136 assert_not_reached ();
1137 }
1138 }

Subscribers

People subscribed via source and target branches

to all changes: