Merge lp:~dobey/pay-ui/iap-support into lp:pay-ui

Proposed by dobey on 2015-09-21
Status: Superseded
Proposed branch: lp:~dobey/pay-ui/iap-support
Merge into: lp:pay-ui
Diff against target: 428 lines (+117/-40)
10 files modified
HACKING (+5/-1)
app/payui.qml (+4/-2)
app/ui/UbuntuPurchaseWebkit.qml (+3/-8)
backend/modules/payui/network.cpp (+52/-13)
backend/modules/payui/network.h (+5/-4)
backend/modules/payui/purchase.cpp (+20/-2)
backend/tests/test_network.cpp (+9/-9)
tests/autopilot/pay_ui/__init__.py (+7/-0)
tests/autopilot/pay_ui/tests/mock_server.py (+1/-1)
tests/autopilot/pay_ui/tests/test_pay_ui.py (+11/-0)
To merge this branch: bzr merge lp:~dobey/pay-ui/iap-support
Reviewer Review Type Date Requested Status
Charles Kerr (community) 2015-09-21 Approve on 2015-10-21
PS Jenkins bot continuous-integration Approve on 2015-10-01
Review via email: mp+271815@code.launchpad.net

This proposal has been superseded by a proposal from 2015-10-30.

Commit Message

Support for performing in-app purchases.

To post a comment you must log in.
lp:~dobey/pay-ui/iap-support updated on 2015-10-01
133. By dobey on 2015-10-01

IAP API returns prices as strings, not floats.

Charles Kerr (charlesk) wrote :

No blockers, a couple of suggestions inline

review: Approve
lp:~dobey/pay-ui/iap-support updated on 2015-10-30
134. By dobey on 2015-10-30

Merge the fix-page-returns branch.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'HACKING'
2--- HACKING 2015-02-26 20:12:22 +0000
3+++ HACKING 2015-10-30 19:32:36 +0000
4@@ -80,7 +80,11 @@
5
6 $ adt-run --click-source . \
7 --click ./com.canonical.payui_15.01.*_amd64.click \
8- -o /tmp/adt-payui-test -U --apt-pocket proposed \
9+ -o /tmp/adt-payui-test \
10+ --setup-commands "add-apt-repository \
11+ ppa:ci-train-ppa-service/stable-phone-overlay" \
12+ --apt-pocket proposed \
13+ --setup-commands "apt-get update" \
14 --setup-commands ubuntu-touch-session \
15 --- qemu ~/adt-vivid-amd64-cloud.img
16
17
18=== modified file 'app/payui.qml'
19--- app/payui.qml 2015-01-12 20:35:11 +0000
20+++ app/payui.qml 2015-10-30 19:32:36 +0000
21@@ -89,7 +89,6 @@
22 checkout.itemTitle = title;
23 checkout.itemSubtitle = publisher;
24 checkout.price = formatted_price;
25- checkCredentials();
26 }
27
28 onPaymentTypesObtained: {
29@@ -176,6 +175,7 @@
30 } else if (mainView.state != "checkout" && !mainView.purchasing && mainView.state != "buy-interaction") {
31 purchase.getPaymentTypes(suggestedCurrency);
32 }
33+ purchase.getItemDetails();
34 }
35
36 onItemNotPurchased: {
37@@ -352,10 +352,12 @@
38
39 PageStack {
40 id: pageStack
41+ objectName: "pageStack"
42+
43 Component.onCompleted: {
44 showLoading();
45 mainView.createDB();
46- purchase.getItemDetails();
47+ purchase.checkCredentials();
48 }
49
50 onCurrentPageChanged: {
51
52=== modified file 'app/ui/UbuntuPurchaseWebkit.qml'
53--- app/ui/UbuntuPurchaseWebkit.qml 2015-03-17 21:01:41 +0000
54+++ app/ui/UbuntuPurchaseWebkit.qml 2015-10-30 19:32:36 +0000
55@@ -91,14 +91,9 @@
56 }
57
58 onUrlChanged: {
59- var base_url_prod = "https://software-center.ubuntu.com";
60- var base_url_staging = "https://sc.staging.ubuntu.com";
61- var base_url_testing = "http://127.0.0.1:[0-9]+";
62- var base_url = "^(" + base_url_prod + "|" + base_url_staging + "|" + base_url_testing + ")/";
63-
64- var re_succeeded = new RegExp(base_url + "click/succeeded");
65- var re_failed = new RegExp(base_url + "click/failed");
66- var re_cancelled = new RegExp(base_url + "click/cancelled");
67+ var re_succeeded = new RegExp("/click/succeeded");
68+ var re_failed = new RegExp("/click/failed");
69+ var re_cancelled = new RegExp("/click/cancelled");
70
71 if (re_succeeded.test(webView.url)) {
72 pageWebkit.purchaseSucceeded();
73
74=== modified file 'backend/modules/payui/network.cpp'
75--- backend/modules/payui/network.cpp 2015-02-06 21:57:58 +0000
76+++ backend/modules/payui/network.cpp 2015-10-30 19:32:36 +0000
77@@ -1,5 +1,5 @@
78 /*
79- * Copyright 2014 Canonical Ltd.
80+ * Copyright 2014-2015 Canonical Ltd.
81 *
82 * This library is free software; you can redistribute it and/or
83 * modify it under the terms of version 3 of the GNU Lesser General Public
84@@ -38,6 +38,8 @@
85 #define PAY_PAYMENTMETHODS_ADD_PATH PAY_PAYMENTMETHODS_PATH + "/add"
86 #define SUGGESTED_CURRENCY_HEADER_NAME "X-Suggested-Currency"
87
88+#define DEVICE_ID_HEADER "X-Device-Id"
89+
90 #define PARTNER_ID_HEADER "X-Partner-ID"
91 #define PARTNER_ID_FILE "/custom/partner-id"
92
93@@ -208,9 +210,18 @@
94 } else if (state->operation.contains(ITEM_INFO) && document.isObject()) {
95 qDebug() << "Reply state: ITEM_INFO";
96 QJsonObject object = document.object();
97+ QString icon;
98+ QString publisher;
99+ if (object.contains("publisher")) {
100+ publisher = object.value("publisher").toString();
101+ }
102+ if (object.contains("icon_url")) {
103+ icon = object.value("icon_url").toString();
104+ } else if (object.contains("icon")) {
105+ icon = object.value("icon").toString();
106+ }
107+
108 QString title = object.value("title").toString();
109- QString publisher = object.value("publisher").toString();
110- QString icon = object.value("icon_url").toString();
111
112 QJsonObject prices = object.value("prices").toObject();
113 QString suggested_currency = DEFAULT_CURRENCY;
114@@ -226,13 +237,25 @@
115 if (isSupportedCurrency(suggested_currency) && prices.contains(suggested_currency)) {
116 currency = suggested_currency;
117 }
118- double price = prices[currency].toDouble();
119+ double price = 0.00;
120+ if (prices[currency].isDouble()) {
121+ price = prices[currency].toDouble();
122+ } else if (prices[currency].isString()) {
123+ price = prices[currency].toString().toDouble();
124+ }
125 QLocale locale;
126 QString formatted_price = locale.toCurrencyString(price, getSymbolForCurrency(currency));
127 qDebug() << "Sending signal: itemDetailsObtained: " << title << " " << formatted_price;
128 Q_EMIT itemDetailsObtained(title, publisher, currency, formatted_price, icon);
129 } else if (state->operation.contains(CHECK_PURCHASED)) {
130- Q_EMIT buyItemSucceeded();
131+ QJsonObject object = document.object();
132+ auto state = object.value("state").toString();
133+ if (state == "Complete" || state == "purchased "||
134+ state == "approved") {
135+ Q_EMIT buyItemSucceeded();
136+ } else {
137+ Q_EMIT itemNotPurchased();
138+ }
139 } else {
140 qDebug() << "Reply received for non valid state.";
141 QString message("Reply received for non valid state.");
142@@ -314,8 +337,10 @@
143 qDebug() << "Payment" << m_selectedAppId << m_selectedBackendId << m_selectedPaymentId;
144 QJsonObject serializer;
145
146- serializer.insert("device_id", getDeviceId());
147 serializer.insert("name", m_selectedAppId);
148+ if (!m_selectedItemId.isEmpty()) {
149+ serializer.insert("item_sku", m_selectedItemId);
150+ }
151 serializer.insert("backend_id", m_selectedBackendId);
152 serializer.insert("method_id", m_selectedPaymentId);
153 serializer.insert("currency", m_currency);
154@@ -325,6 +350,7 @@
155
156 QNetworkRequest request;
157 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
158+ request.setRawHeader(DEVICE_ID_HEADER, getDeviceId().toUtf8().data());
159
160 // Get the partner ID and add it to the request.
161 QByteArray partner_id = getPartnerId();
162@@ -368,17 +394,25 @@
163 return reply.value();
164 }
165
166-void Network::getItemInfo(const QString &packagename)
167+void Network::getItemInfo(const QString& packagename, const QString& sku)
168 {
169- QUrl url(getSearchApiUrl(QString(SEARCH_API_ROOT) + "/package/" + packagename));
170+ QUrl url;
171+
172+ if (sku.isEmpty()) {
173+ url = getSearchApiUrl(QString(SEARCH_API_ROOT) + "/package/" + packagename);
174+ qDebug() << "Request Item Info:" << url;
175+ QUrlQuery query;
176+ query.addQueryItem("fields", "title,description,price,icon_url");
177+ url.setQuery(query);
178+ } else {
179+ url = getPayApiUrl(QString(IAP_API_ROOT) + "/packages/" + packagename + "/items/by-sku/" + sku);
180+ }
181 qDebug() << "Request Item Info:" << url;
182- QUrlQuery query;
183- query.addQueryItem("fields", "title,description,price,icon_url");
184- url.setQuery(query);
185
186 QNetworkRequest request;
187 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
188 request.setUrl(url);
189+ signRequestUrl(request, url.toString(), QStringLiteral("GET"));
190 RequestObject* reqObject = new RequestObject(QString(ITEM_INFO));
191 request.setOriginatingObject(reqObject);
192 m_nam.get(request);
193@@ -427,9 +461,14 @@
194 return m_token.updated();
195 }
196
197-void Network::checkItemPurchased(const QString& appid)
198+void Network::checkItemPurchased(const QString& appid, const QString& sku)
199 {
200- QUrl url(getPayApiUrl(QString(PAY_API_ROOT) + PAY_PURCHASES_PATH + "/" + appid + "/"));
201+ QUrl url;
202+ if (sku.isEmpty()) {
203+ url = getPayApiUrl(QString(PAY_API_ROOT) + PAY_PURCHASES_PATH + "/" + appid + "/");
204+ } else {
205+ url = getPayApiUrl(QString(IAP_API_ROOT) + "/packages/" + appid + "/items/by-sku/" + sku);
206+ }
207
208 qDebug() << "Checking for previous purchase:" << url;
209 QNetworkRequest request;
210
211=== modified file 'backend/modules/payui/network.h'
212--- backend/modules/payui/network.h 2015-02-06 19:16:42 +0000
213+++ backend/modules/payui/network.h 2015-10-30 19:32:36 +0000
214@@ -1,5 +1,5 @@
215 /*
216- * Copyright 2014 Canonical Ltd.
217+ * Copyright 2014-2015 Canonical Ltd.
218 *
219 * This library is free software; you can redistribute it and/or
220 * modify it under the terms of version 3 of the GNU Lesser General Public
221@@ -37,8 +37,9 @@
222 namespace UbuntuPurchase {
223
224 constexpr static const char* PAY_BASE_URL_ENVVAR{"PAY_BASE_URL"};
225-constexpr static const char* PAY_BASE_URL{"https://software-center.ubuntu.com"};
226+constexpr static const char* PAY_BASE_URL{"https://myapps.developer.ubuntu.com"};
227 constexpr static const char* PAY_API_ROOT{"/api/2.0/click"};
228+constexpr static const char* IAP_API_ROOT{"/inventory/api/v1"};
229 constexpr static const char* CURRENCY_ENVVAR {"U1_SEARCH_CURRENCY"};
230 constexpr static const char* SEARCH_BASE_URL_ENVVAR{"U1_SEARCH_BASE_URL"};
231 constexpr static const char* SEARCH_BASE_URL{"https://search.apps.ubuntu.com"};
232@@ -72,14 +73,14 @@
233 const QString& otp,
234 const QString& appid, const QString& itemid, const QString& currency,
235 bool recentLogin);
236- void getItemInfo(const QString &packagename);
237+ void getItemInfo(const QString& packagename, const QString& sku);
238 void checkPassword(const QString& email, const QString& password,
239 const QString& otp, bool purchasing=false);
240 void getCredentials();
241 void setCredentials(Token token);
242 QString getAddPaymentUrl(const QString& currency);
243 QDateTime getTokenUpdated();
244- void checkItemPurchased(const QString& appid);
245+ void checkItemPurchased(const QString& appid, const QString& sku);
246 static QString getSymbolForCurrency(const QString& currency_code);
247 static bool isSupportedCurrency(const QString& currency_code);
248 static QString sanitizeUrl(const QUrl& url);
249
250=== modified file 'backend/modules/payui/purchase.cpp'
251--- backend/modules/payui/purchase.cpp 2014-12-02 04:34:04 +0000
252+++ backend/modules/payui/purchase.cpp 2015-10-30 19:32:36 +0000
253@@ -1,3 +1,21 @@
254+/*
255+ * Copyright 2014-2015 Canonical Ltd.
256+ *
257+ * This library is free software; you can redistribute it and/or
258+ * modify it under the terms of version 3 of the GNU Lesser General Public
259+ * License as published by the Free Software Foundation.
260+ *
261+ * This program is distributed in the hope that it will be useful,
262+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
263+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
264+ * General Public License for more details.
265+ *
266+ * You should have received a copy of the GNU Lesser General Public
267+ * License along with this library; if not, write to the
268+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
269+ * Boston, MA 02110-1301, USA.
270+ */
271+
272 #include "purchase.h"
273 #include <QUrl>
274 #include <QUrlQuery>
275@@ -93,7 +111,7 @@
276 quitCancel();
277 }
278 qDebug() << "Getting Item Details";
279- m_network.getItemInfo(m_appid);
280+ m_network.getItemInfo(m_appid, m_itemid);
281 }
282
283 void Purchase::getPaymentTypes(const QString& currency)
284@@ -123,7 +141,7 @@
285
286 void Purchase::checkItemPurchased()
287 {
288- m_network.checkItemPurchased(m_appid);
289+ m_network.checkItemPurchased(m_appid, m_itemid);
290 }
291
292 }
293
294=== modified file 'backend/tests/test_network.cpp'
295--- backend/tests/test_network.cpp 2015-02-06 21:57:58 +0000
296+++ backend/tests/test_network.cpp 2015-10-30 19:32:36 +0000
297@@ -1,5 +1,5 @@
298 /*
299- * Copyright 2014 Canonical Ltd.
300+ * Copyright 2014-2015 Canonical Ltd.
301 *
302 * This library is free software; you can redistribute it and/or
303 * modify it under the terms of version 3 of the GNU Lesser General Public
304@@ -196,7 +196,7 @@
305 setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/iteminfo/", 1);
306 unsetenv(CURRENCY_ENVVAR);
307 QSignalSpy spy(&network, SIGNAL(itemDetailsObtained(QString,QString,QString,QString,QString)));
308- network.getItemInfo("packagename");
309+ network.getItemInfo("packagename", "");
310 QTRY_COMPARE(spy.count(), 1);
311 QList<QVariant> arguments = spy.takeFirst();
312 QCOMPARE(arguments.at(2).toString(), QStringLiteral("USD"));
313@@ -208,7 +208,7 @@
314 setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/iteminfo/eurozone/", 1);
315 unsetenv(CURRENCY_ENVVAR);
316 QSignalSpy spy(&network, SIGNAL(itemDetailsObtained(QString,QString,QString,QString,QString)));
317- network.getItemInfo("packagename");
318+ network.getItemInfo("packagename", "");
319 QTRY_COMPARE(spy.count(), 1);
320 QList<QVariant> arguments = spy.takeFirst();
321 QCOMPARE(arguments.at(2).toString(), QStringLiteral("EUR"));
322@@ -220,7 +220,7 @@
323 setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/iteminfo/dotar/", 1);
324 unsetenv(CURRENCY_ENVVAR);
325 QSignalSpy spy(&network, SIGNAL(itemDetailsObtained(QString,QString,QString,QString,QString)));
326- network.getItemInfo("packagename");
327+ network.getItemInfo("packagename", "");
328 QTRY_COMPARE(spy.count(), 1);
329 QList<QVariant> arguments = spy.takeFirst();
330 QCOMPARE(arguments.at(2).toString(), QStringLiteral("USD"));
331@@ -232,7 +232,7 @@
332 setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/iteminfo/", 1);
333 setenv(CURRENCY_ENVVAR, "EUR", true);
334 QSignalSpy spy(&network, SIGNAL(itemDetailsObtained(QString,QString,QString,QString,QString)));
335- network.getItemInfo("packagename");
336+ network.getItemInfo("packagename", "");
337 QTRY_COMPARE(spy.count(), 1);
338 QList<QVariant> arguments = spy.takeFirst();
339 QCOMPARE(arguments.at(2).toString(), QStringLiteral("EUR"));
340@@ -245,7 +245,7 @@
341 setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/iteminfo/dotar/", 1);
342 setenv(CURRENCY_ENVVAR, "EUR", true);
343 QSignalSpy spy(&network, SIGNAL(itemDetailsObtained(QString,QString,QString,QString,QString)));
344- network.getItemInfo("packagename");
345+ network.getItemInfo("packagename", "");
346 QTRY_COMPARE(spy.count(), 1);
347 QList<QVariant> arguments = spy.takeFirst();
348 QCOMPARE(arguments.at(2).toString(), QStringLiteral("EUR"));
349@@ -258,7 +258,7 @@
350 setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/fail/iteminfo/", 1);
351 unsetenv(CURRENCY_ENVVAR);
352 QSignalSpy spy(&network, SIGNAL(error(QString)));
353- network.getItemInfo("packagename");
354+ network.getItemInfo("packagename", "");
355 QTRY_COMPARE(spy.count(), 1);
356 }
357
358@@ -274,7 +274,7 @@
359 {
360 setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/", 1);
361 QSignalSpy spy(&network, SIGNAL(buyItemSucceeded()));
362- network.checkItemPurchased("com.example.fakeapp");
363+ network.checkItemPurchased("com.example.fakeapp", "");
364 QTRY_COMPARE(spy.count(), 1);
365 }
366
367@@ -282,7 +282,7 @@
368 {
369 setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/notpurchased/", 1);
370 QSignalSpy spy(&network, SIGNAL(itemNotPurchased()));
371- network.checkItemPurchased("com.example.fakeapp");
372+ network.checkItemPurchased("com.example.fakeapp", "");
373 QTRY_COMPARE(spy.count(), 1);
374 }
375
376
377=== modified file 'tests/autopilot/pay_ui/__init__.py'
378--- tests/autopilot/pay_ui/__init__.py 2015-04-14 17:10:20 +0000
379+++ tests/autopilot/pay_ui/__init__.py 2015-10-30 19:32:36 +0000
380@@ -74,6 +74,13 @@
381 return self.wait_select_single(objectName='paymentTypes')
382
383 @autopilot_logging.log_action(logger.info)
384+ def get_checkout_page(self):
385+ """Get the widget for the Checkout page.
386+
387+ :return the Checkout page widget."""
388+ return self.wait_select_single(objectName='pageCheckout')
389+
390+ @autopilot_logging.log_action(logger.info)
391 def tap_dialog_ok_button(self):
392 """Tap the 'OK' button in the dialog."""
393 self._tap('dialogOkButton')
394
395=== modified file 'tests/autopilot/pay_ui/tests/mock_server.py'
396--- tests/autopilot/pay_ui/tests/mock_server.py 2015-03-25 20:08:42 +0000
397+++ tests/autopilot/pay_ui/tests/mock_server.py 2015-10-30 19:32:36 +0000
398@@ -31,7 +31,7 @@
399
400 html_cancel = """
401 <html>
402- <body bgcolor="red" onClick="window.location.assign('/click/cancelled')">
403+ <body bgcolor="red" onClick="window.location.assign('/api/2.0/click/cancelled')">
404 <h1>Placeholder for web interaction</h1>
405 <p>Click anywhere to cancel</p>
406 </body>
407
408=== modified file 'tests/autopilot/pay_ui/tests/test_pay_ui.py'
409--- tests/autopilot/pay_ui/tests/test_pay_ui.py 2015-04-13 21:11:24 +0000
410+++ tests/autopilot/pay_ui/tests/test_pay_ui.py 2015-10-30 19:32:36 +0000
411@@ -65,6 +65,17 @@
412 self.main_view.tap_on_webview()
413 self.assertThat(payment_types.get_option_count, Eventually(Equals(4)))
414
415+ def test_add_credit_card_returns_on_cancel(self):
416+ self.mock_server.set_interaction_result_cancelled()
417+ payment_types = self.main_view.get_payment_types()
418+ self.assertThat(payment_types.get_option_count(), Equals(3))
419+ self.main_view.open_add_card_page()
420+ self.take_screenshot('add_card_page')
421+ self.main_view.tap_on_webview()
422+ checkout_page = self.main_view.get_checkout_page()
423+ self.assertThat(checkout_page.get_properties()["active"],
424+ Eventually(Equals(True)))
425+
426 def test_add_credit_card_cancelled(self):
427 self.mock_server.set_interaction_result_cancelled()
428 payment_types = self.main_view.get_payment_types()

Subscribers

People subscribed via source and target branches

to all changes: