Merge lp:~dobey/pay-service/merge-payui into lp:pay-service

Proposed by dobey
Status: Merged
Approved by: dobey
Approved revision: 121
Merged at revision: 96
Proposed branch: lp:~dobey/pay-service/merge-payui
Merge into: lp:pay-service
Diff against target: 10389 lines (+9058/-472)
107 files modified
.bzrignore (+3/-2)
CMakeLists.txt (+1/-2)
HACKING (+85/-0)
debian/control (+22/-4)
debian/copyright (+2/-2)
debian/pay-service.click-hook (+0/-4)
debian/pay-service.install (+2/-2)
debian/pay-ui.install (+3/-0)
debian/rules (+1/-1)
pay-ui/CMakeLists.txt (+21/-0)
pay-ui/app/CMakeLists.txt (+6/-0)
pay-ui/app/components/AlertDialog.qml (+31/-0)
pay-ui/app/components/BeforeUnloadDialog.qml (+37/-0)
pay-ui/app/components/CMakeLists.txt (+6/-0)
pay-ui/app/components/ConfirmDialog.qml (+37/-0)
pay-ui/app/components/ModalDialog.qml (+32/-0)
pay-ui/app/components/PromptDialog.qml (+51/-0)
pay-ui/app/components/SecurityCertificatePopover.qml (+145/-0)
pay-ui/app/payui.qml (+459/-0)
pay-ui/app/tests/unit/js/unit_test.js (+17/-0)
pay-ui/app/tests/unit/tst_checkoutpage.qml (+73/-0)
pay-ui/app/tests/unit/tst_purchasewebkit.qml (+57/-0)
pay-ui/app/ui/CMakeLists.txt (+6/-0)
pay-ui/app/ui/CheckoutPage.qml (+404/-0)
pay-ui/app/ui/ErrorDialog.qml (+67/-0)
pay-ui/app/ui/UbuntuPurchaseWebkit.qml (+107/-0)
pay-ui/backend/CMakeLists.txt (+49/-0)
pay-ui/backend/modules/payui/backend.cpp (+23/-0)
pay-ui/backend/modules/payui/backend.h (+33/-0)
pay-ui/backend/modules/payui/certificateadapter.cpp (+46/-0)
pay-ui/backend/modules/payui/certificateadapter.h (+51/-0)
pay-ui/backend/modules/payui/credentials_service.cpp (+81/-0)
pay-ui/backend/modules/payui/credentials_service.h (+53/-0)
pay-ui/backend/modules/payui/network.cpp (+512/-0)
pay-ui/backend/modules/payui/network.h (+136/-0)
pay-ui/backend/modules/payui/oxideconstants.cpp (+19/-0)
pay-ui/backend/modules/payui/oxideconstants.h (+53/-0)
pay-ui/backend/modules/payui/pay_info.cpp (+37/-0)
pay-ui/backend/modules/payui/pay_info.h (+73/-0)
pay-ui/backend/modules/payui/purchase.cpp (+154/-0)
pay-ui/backend/modules/payui/purchase.h (+59/-0)
pay-ui/backend/modules/payui/qmldir (+2/-0)
pay-ui/backend/tests/CMakeLists.txt (+30/-0)
pay-ui/backend/tests/mock_click_server.py (+162/-0)
pay-ui/backend/tests/test_network.cpp (+317/-0)
pay-ui/pay-ui.in (+19/-0)
pay-ui/tests/autopilot/pay_ui/__init__.py (+106/-0)
pay-ui/tests/autopilot/pay_ui/tests/__init__.py (+77/-0)
pay-ui/tests/autopilot/pay_ui/tests/mock_server.py (+337/-0)
pay-ui/tests/autopilot/pay_ui/tests/test_pay_ui.py (+186/-0)
pay-ui/tests/autopilot/run_autopilot (+1/-0)
po/CMakeLists.txt (+1/-1)
po/POTFILES.in (+8/-0)
po/aa.po (+106/-0)
po/am.po (+106/-0)
po/ast.po (+106/-0)
po/az.po (+106/-0)
po/br.po (+106/-0)
po/ca.po (+106/-0)
po/ca@valencia.po (+106/-0)
po/cs.po (+106/-0)
po/cy.po (+106/-0)
po/de.po (+106/-0)
po/el.po (+106/-0)
po/en_AU.po (+106/-0)
po/en_GB.po (+106/-0)
po/es.po (+106/-0)
po/eu.po (+106/-0)
po/fa.po (+106/-0)
po/fi.po (+106/-0)
po/fr.po (+106/-0)
po/gd.po (+107/-0)
po/gl.po (+106/-0)
po/he.po (+106/-0)
po/hu.po (+106/-0)
po/id.po (+106/-0)
po/is.po (+106/-0)
po/it.po (+106/-0)
po/ja.po (+106/-0)
po/ko.po (+106/-0)
po/nb.po (+106/-0)
po/nl.po (+106/-0)
po/pay-service.pot (+82/-2)
po/pl.po (+106/-0)
po/pt.po (+106/-0)
po/pt_BR.po (+106/-0)
po/ro.po (+106/-0)
po/ru.po (+106/-0)
po/sl.po (+106/-0)
po/sq.po (+106/-0)
po/sr.po (+106/-0)
po/sv.po (+107/-0)
po/ug.po (+106/-0)
po/uk.po (+106/-0)
po/vi.po (+106/-0)
po/zh_CN.po (+106/-0)
po/zh_TW.po (+106/-0)
service-ng/src/launchpad.net/go-mir/mir/fakes/prompt_session.go (+5/-1)
service-ng/src/launchpad.net/go-mir/mir/mir_prompt_session_helper.c (+41/-0)
service-ng/src/launchpad.net/go-mir/mir/mir_prompt_session_helper.h (+28/-0)
service-ng/src/launchpad.net/go-mir/mir/prompt_session.go (+14/-1)
service-ng/src/pay-service-2/service/pay_ui.go (+18/-83)
service-ng/src/pay-service-2/service/pay_ui_test.go (+8/-199)
service-ng/test-service.sh (+2/-2)
tests/setup-staging.sh (+0/-8)
ual-helper/CMakeLists.txt (+0/-12)
ual-helper/exec-tool.c (+0/-146)
To merge this branch: bzr merge lp:~dobey/pay-service/merge-payui
Reviewer Review Type Date Requested Status
Charles Kerr (community) Approve
PS Jenkins bot (community) continuous-integration Needs Fixing
Review via email: mp+288514@code.launchpad.net

Commit message

Merge pay-ui into pay-service.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Charles Kerr (charlesk) wrote :

LGTM.

Reviewed the glue code, eg the packaging changes, pay-ui launcher changes etc. I didn't review the code in pay-ui/ since it's not new & is just being merged over from a different repo.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2015-12-11 19:49:20 +0000
3+++ .bzrignore 2016-03-11 14:49:24 +0000
4@@ -8,7 +8,8 @@
5 *.so.*
6 *.mo
7 *.service
8-
9-
10+pay-ui/pay-ui
11+
12+__pycache__
13 build/
14 builddir/
15
16=== modified file 'CMakeLists.txt'
17--- CMakeLists.txt 2015-12-13 02:28:18 +0000
18+++ CMakeLists.txt 2016-03-11 14:49:24 +0000
19@@ -35,7 +35,6 @@
20
21 pkg_check_modules (SERVICE_DEPS REQUIRED
22 Qt5Core
23- click-0.4
24 dbus-cpp
25 dbustest-1
26 gio-2.0
27@@ -87,12 +86,12 @@
28 add_subdirectory(common)
29 add_subdirectory(libpay)
30 add_subdirectory(service-ng)
31-add_subdirectory(ual-helper)
32 add_subdirectory(data)
33 if (${enable_tests})
34 add_subdirectory(tests)
35 endif ()
36 add_subdirectory(pay-test-app)
37+add_subdirectory(pay-ui)
38 add_subdirectory(po)
39
40 ##
41
42=== added file 'HACKING'
43--- HACKING 1970-01-01 00:00:00 +0000
44+++ HACKING 2016-03-11 14:49:24 +0000
45@@ -0,0 +1,85 @@
46+pay-service hacking guide
47+===============================
48+
49+Getting pay-service
50+-------------------------
51+
52+To get the main branch of pay-service:
53+
54+ $ bzr branch lp:pay-service
55+
56+
57+Getting dependencies
58+--------------------
59+
60+To succesfully build pay-service extra packages are required:
61+
62+ $ sudo apt-get build-dep pay-service
63+
64+
65+Building pay-service
66+------------------
67+
68+This app is built using cmake. Here's an example on how to build it:
69+
70+ $ mkdir build
71+ $ cd build
72+ $ cmake ..
73+ $ make -j 8
74+
75+
76+Running the unit tests
77+----------------------
78+
79+ $ make test
80+
81+
82+Running the autopilot tests
83+---------------------------
84+
85+To run the autopilot tests locally, you first need to build pay-service.
86+
87+ $ make autopilot
88+
89+The autopilot tests can also be run against a built debian package, under
90+qemu. To do so, you will need some additional packages. Most importantly,
91+you will need the latest version of the autopkgtest package. You can download
92+it at https://launchpad.net/ubuntu/+source/autopkgtest by selecting the most
93+recent build for the most recent version of Ubuntu.
94+
95+ $ sudo dpkg -i autopkgtest*.deb
96+ $ sudo apt-get install qemu
97+
98+After installing autopkgtest and qemu, you need to build an image for qemu
99+to use. We use vivid here, as building an image to closely resemble the actual
100+stable phone images is quite difficult. When this is easier in the future, we
101+will switch to using stable phone images for this. The architecture argument
102+should match the architecture of the debian package you are trying to test.
103+We output the image to ~/ rather than the current directory, so it will be in
104+a safer place to avoid rebuilding images every time. You can store it in any
105+directory you wish.
106+
107+ $ adt-buildvm-ubuntu-cloud -r vivid -a amd64 -o ~/
108+
109+Then the tests may be run using adt-run with the qemu virtualization host.
110+The output directory option here can be wherever you like, and is where the
111+test artifacts will be placed. The ordering of the arguments to adt-run is
112+important so try to keep them in this order.
113+
114+ $ adt-run --click-source . \
115+ --source ../pay-service*.dsc \
116+ -o /tmp/adt-payui-test \
117+ --setup-commands "add-apt-repository \
118+ ppa:ci-train-ppa-service/stable-phone-overlay" \
119+ --apt-pocket proposed \
120+ --setup-commands "apt-get update" \
121+ --setup-commands ubuntu-touch-session \
122+ --- qemu ~/adt-vivid-amd64-cloud.img
123+
124+To examine the test results, which are in subunit format, additional tools are
125+required.
126+
127+ $ sudo add-apt-repository ppa:thomir/trv
128+ $ sudo apt-get update
129+ $ sudo apt-get install trv
130+ $ trv /tmp/adt-payui-test/artifacts/autopilot.subunit
131
132=== modified file 'debian/control'
133--- debian/control 2015-12-11 22:14:01 +0000
134+++ debian/control 2016-03-11 14:49:24 +0000
135@@ -2,8 +2,7 @@
136 Section: gnome
137 Priority: optional
138 Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
139-Build-Depends: click-dev,
140- cmake,
141+Build-Depends: cmake,
142 cmake-extras,
143 dbus,
144 dbus-test-runner,
145@@ -15,7 +14,6 @@
146 google-mock,
147 intltool,
148 lcov,
149- libclick-0.4-dev,
150 libdbus-1-dev,
151 libdbus-cpp-dev,
152 libdbustest1-dev,
153@@ -26,12 +24,15 @@
154 libproperties-cpp-dev,
155 libtrust-store-dev,
156 libubuntu-app-launch2-dev (>= 0.5),
157+ libubuntuoneauth-2.0-dev,
158 pkg-config,
159 python3-dbusmock,
160 qt5-default,
161 qtbase5-dev,
162 qtdeclarative5-dev,
163+ qtdeclarative5-dev-tools,
164 sysvinit-utils,
165+ xvfb,
166 Standards-Version: 3.9.5
167 Homepage: https://launchpad.net/pay-service
168 # If you aren't a member of ~indicator-applet-developers but need to upload
169@@ -44,6 +45,7 @@
170 Pre-Depends: ${misc:Pre-Depends},
171 Depends: ${misc:Depends},
172 ${shlibs:Depends},
173+ pay-ui (= ${binary:Version}),
174 sysvinit-utils,
175 ubuntu-push-client (>= 0.68+15.04.20151009),
176 Recommends: gnupg,
177@@ -54,7 +56,23 @@
178 payment provider and give that money to the developer. This service
179 coordinates the payment interaction with the server on the device.
180 .
181- This package provides a service for the Pay Service
182+ This package provides a service for the Pay Service.
183+
184+Package: pay-ui
185+Architecture: any
186+Depends:
187+ ${misc:Depends},
188+ ${shlibs:Depends},
189+ qtdeclarative5-ubuntu-web-plugin [amd64 armhf i386],
190+ qtdeclarative5-online-accounts-client0.1,
191+ qtdeclarative5-ubuntu-ui-toolkit-plugin,
192+Description: service to allow requesting payment for an item - user interface
193+ Allows applications to request an item be paid for by the user.
194+ This requires interaction with the server backend to handle the
195+ payment provider and give that money to the developer. This service
196+ coordinates the payment interaction with the server on the device.
197+ .
198+ This package provides the user interface for the Pay Service.
199
200 Package: libpay2
201 Architecture: any
202
203=== modified file 'debian/copyright'
204--- debian/copyright 2015-12-10 22:04:32 +0000
205+++ debian/copyright 2016-03-11 14:49:24 +0000
206@@ -3,7 +3,7 @@
207 Source: http://launchpad.net/pay-service
208
209 Files: *
210-Copyright: 2014-2015 Canonical, Ltd.
211+Copyright: 2014-2016 Canonical, Ltd.
212 License: GPL-3
213
214 Files: libpay/*
215@@ -27,7 +27,7 @@
216 License: LGPL-3
217
218 Files: service-ng/src/launchpad.net/go-mir/*
219-Copyright: 2015 Canonical, Ltd.
220+Copyright: 2015-2016 Canonical, Ltd.
221 License: LGPL-3
222
223 Files: service-ng/src/launchpad.net/go-trust-store/*
224
225=== removed file 'debian/pay-service.click-hook'
226--- debian/pay-service.click-hook 2014-07-10 13:34:44 +0000
227+++ debian/pay-service.click-hook 1970-01-01 00:00:00 +0000
228@@ -1,4 +0,0 @@
229-Pattern: ${home}/.cache/pay-service/pay-ui/${id}.desktop
230-Exec: /bin/true
231-User-Level: yes
232-Hook-Name: pay-ui
233
234=== modified file 'debian/pay-service.install'
235--- debian/pay-service.install 2015-12-09 18:18:45 +0000
236+++ debian/pay-service.install 2016-03-11 14:49:24 +0000
237@@ -1,5 +1,5 @@
238 usr/lib/*/pay-service/pay-service-2
239 usr/lib/*/pay-service/setup-staging.sh
240-usr/lib/*/ubuntu-app-launch/pay-ui/*
241+usr/share/locale/*/LC_MESSAGES/pay-service.mo
242 usr/share/dbus-1/services/*.service
243-usr/share/upstart/sessions/*.conf
244\ No newline at end of file
245+usr/share/upstart/sessions/*.conf
246
247=== added file 'debian/pay-ui.install'
248--- debian/pay-ui.install 1970-01-01 00:00:00 +0000
249+++ debian/pay-ui.install 2016-03-11 14:49:24 +0000
250@@ -0,0 +1,3 @@
251+usr/lib/payui
252+usr/lib/*/payui
253+usr/share/payui
254
255=== modified file 'debian/rules'
256--- debian/rules 2015-10-14 20:53:54 +0000
257+++ debian/rules 2016-03-11 14:49:24 +0000
258@@ -10,4 +10,4 @@
259 dh_auto_configure -- -DCMAKE_INSTALL_LIBEXECDIR=/usr/lib/$(DEB_HOST_MULTIARCH)/pay-service
260
261 %:
262- dh $@ --parallel --fail-missing --with click
263+ dh $@ --parallel --fail-missing
264
265=== added directory 'pay-ui'
266=== added file 'pay-ui/CMakeLists.txt'
267--- pay-ui/CMakeLists.txt 1970-01-01 00:00:00 +0000
268+++ pay-ui/CMakeLists.txt 2016-03-11 14:49:24 +0000
269@@ -0,0 +1,21 @@
270+# Need xvfb-run for some tests.
271+set(XVFB_CMD xvfb-run -a -s "-screen 0 540x960x24")
272+
273+# Standard install paths
274+set(APP_NAME payui)
275+
276+set(QT_IMPORTS_DIR "${CMAKE_INSTALL_FULL_LIBEXECDIR}/payui")
277+set(PAYUI_DIR "${CMAKE_INSTALL_FULL_DATADIR}/payui/qml")
278+
279+add_subdirectory(app)
280+add_subdirectory(backend)
281+
282+configure_file(pay-ui.in pay-ui)
283+install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/pay-ui
284+ DESTINATION lib/payui
285+)
286+
287+add_custom_target("autopilot"
288+ COMMAND PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}/tests/autopilot BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR} SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} U1_DEBUG=1 ${XVFB_CMD} ${CMAKE_CURRENT_SOURCE_DIR}/tests/autopilot/run_autopilot
289+ DEPENDS payuibackend payuibackend-qmldir
290+)
291
292=== added directory 'pay-ui/app'
293=== added file 'pay-ui/app/CMakeLists.txt'
294--- pay-ui/app/CMakeLists.txt 1970-01-01 00:00:00 +0000
295+++ pay-ui/app/CMakeLists.txt 2016-03-11 14:49:24 +0000
296@@ -0,0 +1,6 @@
297+file(GLOB QML_JS_FILES *.qml *.js)
298+
299+install(FILES ${QML_JS_FILES} DESTINATION ${PAYUI_DIR})
300+
301+add_subdirectory(components)
302+add_subdirectory(ui)
303
304=== added directory 'pay-ui/app/components'
305=== added file 'pay-ui/app/components/AlertDialog.qml'
306--- pay-ui/app/components/AlertDialog.qml 1970-01-01 00:00:00 +0000
307+++ pay-ui/app/components/AlertDialog.qml 2016-03-11 14:49:24 +0000
308@@ -0,0 +1,31 @@
309+/*
310+ * Copyright 2013-2014 Canonical Ltd.
311+ *
312+ * This file is part of webbrowser-app.
313+ *
314+ * webbrowser-app is free software; you can redistribute it and/or modify
315+ * it under the terms of the GNU General Public License as published by
316+ * the Free Software Foundation; version 3.
317+ *
318+ * webbrowser-app is distributed in the hope that it will be useful,
319+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
320+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
321+ * GNU General Public License for more details.
322+ *
323+ * You should have received a copy of the GNU General Public License
324+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
325+ */
326+
327+import QtQuick 2.0
328+import Ubuntu.Components 1.1
329+
330+ModalDialog {
331+ objectName: "alertDialog"
332+ title: i18n.dtr("webbrowser-app", "JavaScript Alert")
333+
334+ Button {
335+ objectName: "dialogOkButton"
336+ text: i18n.dtr("webbrowser-app", "OK")
337+ onClicked: model.accept()
338+ }
339+}
340
341=== added file 'pay-ui/app/components/BeforeUnloadDialog.qml'
342--- pay-ui/app/components/BeforeUnloadDialog.qml 1970-01-01 00:00:00 +0000
343+++ pay-ui/app/components/BeforeUnloadDialog.qml 2016-03-11 14:49:24 +0000
344@@ -0,0 +1,37 @@
345+/*
346+ * Copyright 2014 Canonical Ltd.
347+ *
348+ * This file is part of webbrowser-app.
349+ *
350+ * webbrowser-app is free software; you can redistribute it and/or modify
351+ * it under the terms of the GNU General Public License as published by
352+ * the Free Software Foundation; version 3.
353+ *
354+ * webbrowser-app is distributed in the hope that it will be useful,
355+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
356+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
357+ * GNU General Public License for more details.
358+ *
359+ * You should have received a copy of the GNU General Public License
360+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
361+ */
362+
363+import QtQuick 2.0
364+import Ubuntu.Components 1.1
365+
366+ModalDialog {
367+ title: i18n.dtr("webbrowser-app", "Confirm Navigation")
368+ objectName: "beforeUnloadDialog"
369+
370+ Button {
371+ objectName: "leaveButton"
372+ text: i18n.dtr("webbrowser-app", "Leave")
373+ onClicked: model.accept()
374+ }
375+
376+ Button {
377+ objectName: "stayButton"
378+ text: i18n.dtr("webbrowser-app", "Stay")
379+ onClicked: model.reject()
380+ }
381+}
382
383=== added file 'pay-ui/app/components/CMakeLists.txt'
384--- pay-ui/app/components/CMakeLists.txt 1970-01-01 00:00:00 +0000
385+++ pay-ui/app/components/CMakeLists.txt 2016-03-11 14:49:24 +0000
386@@ -0,0 +1,6 @@
387+file(GLOB COMPONENTS_QML_JS_FILES *.qml *.js)
388+
389+# make the files visible in the qtcreator tree
390+add_custom_target(payui_components_QMlFiles ALL SOURCES ${COMPONENTS_QML_JS_FILES})
391+
392+install(FILES ${COMPONENTS_QML_JS_FILES} DESTINATION ${PAYUI_DIR}/components)
393
394=== added file 'pay-ui/app/components/ConfirmDialog.qml'
395--- pay-ui/app/components/ConfirmDialog.qml 1970-01-01 00:00:00 +0000
396+++ pay-ui/app/components/ConfirmDialog.qml 2016-03-11 14:49:24 +0000
397@@ -0,0 +1,37 @@
398+/*
399+ * Copyright 2013-2014 Canonical Ltd.
400+ *
401+ * This file is part of webbrowser-app.
402+ *
403+ * webbrowser-app is free software; you can redistribute it and/or modify
404+ * it under the terms of the GNU General Public License as published by
405+ * the Free Software Foundation; version 3.
406+ *
407+ * webbrowser-app is distributed in the hope that it will be useful,
408+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
409+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
410+ * GNU General Public License for more details.
411+ *
412+ * You should have received a copy of the GNU General Public License
413+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
414+ */
415+
416+import QtQuick 2.0
417+import Ubuntu.Components 1.1
418+
419+ModalDialog {
420+ objectName: "confirmDialog"
421+ title: i18n.dtr("webbrowser-app", "JavaScript Confirmation")
422+
423+ Button {
424+ objectName: "dialogOkButton"
425+ text: i18n.dtr("webbrowser-app", "OK")
426+ onClicked: model.accept()
427+ }
428+
429+ Button {
430+ objectName: "dialogCancelButton"
431+ text: i18n.dtr("webbrowser-app", "Cancel")
432+ onClicked: model.reject()
433+ }
434+}
435
436=== added file 'pay-ui/app/components/ModalDialog.qml'
437--- pay-ui/app/components/ModalDialog.qml 1970-01-01 00:00:00 +0000
438+++ pay-ui/app/components/ModalDialog.qml 2016-03-11 14:49:24 +0000
439@@ -0,0 +1,32 @@
440+/*
441+ * Copyright 2014 Canonical Ltd.
442+ *
443+ * This file is part of webbrowser-app.
444+ *
445+ * webbrowser-app is free software; you can redistribute it and/or modify
446+ * it under the terms of the GNU General Public License as published by
447+ * the Free Software Foundation; version 3.
448+ *
449+ * webbrowser-app is distributed in the hope that it will be useful,
450+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
451+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
452+ * GNU General Public License for more details.
453+ *
454+ * You should have received a copy of the GNU General Public License
455+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
456+ */
457+
458+import QtQuick 2.0
459+import Ubuntu.Components 1.1
460+import Ubuntu.Components.Popups 1.0 as Popups
461+
462+Popups.Dialog {
463+ text: model.message
464+
465+ // Set the parent at construction time, instead of letting show()
466+ // set it later on, which for some reason results in the size of
467+ // the dialog not being updated.
468+ parent: QuickUtils.rootItem(this)
469+
470+ Component.onCompleted: show()
471+}
472
473=== added file 'pay-ui/app/components/PromptDialog.qml'
474--- pay-ui/app/components/PromptDialog.qml 1970-01-01 00:00:00 +0000
475+++ pay-ui/app/components/PromptDialog.qml 2016-03-11 14:49:24 +0000
476@@ -0,0 +1,51 @@
477+/*
478+ * Copyright 2013-2014 Canonical Ltd.
479+ *
480+ * This file is part of webbrowser-app.
481+ *
482+ * webbrowser-app is free software; you can redistribute it and/or modify
483+ * it under the terms of the GNU General Public License as published by
484+ * the Free Software Foundation; version 3.
485+ *
486+ * webbrowser-app is distributed in the hope that it will be useful,
487+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
488+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
489+ * GNU General Public License for more details.
490+ *
491+ * You should have received a copy of the GNU General Public License
492+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
493+ */
494+
495+import QtQuick 2.0
496+import Ubuntu.Components 1.1
497+
498+ModalDialog {
499+ title: i18n.dtr("webbrowser-app", "JavaScript Prompt")
500+
501+ TextField {
502+ objectName: "dialogInput"
503+ id: input
504+ text: model.defaultValue
505+ onAccepted: model.accept(input.text)
506+ }
507+
508+ Button {
509+ objectName: "dialogOkButton"
510+ text: i18n.dtr("webbrowser-app", "OK")
511+ color: "green"
512+ onClicked: model.accept(input.text)
513+ }
514+
515+ Button {
516+ objectName: "dialogCancelButton"
517+ text: i18n.dtr("webbrowser-app", "Cancel")
518+ color: UbuntuColors.coolGrey
519+ onClicked: model.reject()
520+ }
521+
522+ Binding {
523+ target: model
524+ property: "currentValue"
525+ value: input.text
526+ }
527+}
528
529=== added file 'pay-ui/app/components/SecurityCertificatePopover.qml'
530--- pay-ui/app/components/SecurityCertificatePopover.qml 1970-01-01 00:00:00 +0000
531+++ pay-ui/app/components/SecurityCertificatePopover.qml 2016-03-11 14:49:24 +0000
532@@ -0,0 +1,145 @@
533+/*
534+ * Copyright 2014 Canonical Ltd.
535+ *
536+ * This file is part of webbrowser-app.
537+ *
538+ * webbrowser-app is free software; you can redistribute it and/or modify
539+ * it under the terms of the GNU General Public License as published by
540+ * the Free Software Foundation; version 3.
541+ *
542+ * webbrowser-app is distributed in the hope that it will be useful,
543+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
544+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
545+ * GNU General Public License for more details.
546+ *
547+ * You should have received a copy of the GNU General Public License
548+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
549+ */
550+
551+import QtQuick 2.0
552+import Ubuntu.Components 1.1
553+import Ubuntu.Components.ListItems 1.0
554+import Ubuntu.Components.Popups 1.0
555+import payui 0.1 as Oxide
556+
557+Popover {
558+ id: certificatePopover
559+
560+ property var securityStatus
561+
562+ Column {
563+ width: parent.width - units.gu(4)
564+ anchors.horizontalCenter: parent.horizontalCenter
565+ spacing: units.gu(0.5)
566+
567+ Item {
568+ height: units.gu(1.5)
569+ width: parent.width
570+ }
571+
572+ Column {
573+ width: parent.width
574+ visible: securityStatus.securityLevel == Oxide.SecurityStatus.SecurityLevelWarning
575+ spacing: units.gu(0.5)
576+
577+ Row {
578+ width: parent.width
579+ spacing: units.gu(0.5)
580+
581+ Icon {
582+ name: "security-alert"
583+ height: units.gu(2)
584+ width: height
585+ }
586+
587+ Label {
588+ width: parent.width
589+ wrapMode: Text.WordWrap
590+ text: i18n.dtr("webbrowser-app", "This site has insecure content")
591+ fontSize: "x-small"
592+ }
593+ }
594+
595+ ThinDivider {
596+ width: parent.width
597+ anchors.leftMargin: 0
598+ anchors.rightMargin: 0
599+ }
600+ }
601+
602+ Label {
603+ width: parent.width
604+ wrapMode: Text.WordWrap
605+ text: i18n.dtr("webbrowser-app", "You are connected to")
606+ fontSize: "x-small"
607+ }
608+
609+ Label {
610+ width: parent.width
611+ wrapMode: Text.WordWrap
612+ text: securityStatus.certificate.subjectDisplayName
613+ fontSize: "x-small"
614+ }
615+
616+ ThinDivider {
617+ width: parent.width
618+ anchors.leftMargin: 0
619+ anchors.rightMargin: 0
620+ visible: orgName.visible || localityName.visible || stateName.visible || countryName.visible
621+ }
622+
623+ Label {
624+ width: parent.width
625+ wrapMode: Text.WordWrap
626+ visible: orgName.visible
627+ text: i18n.dtr("webbrowser-app", "Which is run by")
628+ fontSize: "x-small"
629+ }
630+
631+ Label {
632+ id: orgName
633+ width: parent.width
634+ wrapMode: Text.WordWrap
635+ visible: text.length > 0
636+ text: securityStatus.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrOrganizationName).join(", ")
637+ fontSize: "x-small"
638+ }
639+
640+ Label {
641+ id: localityName
642+ width: parent.width
643+ wrapMode: Text.WordWrap
644+ visible: text.length > 0
645+ text: securityStatus.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrLocalityName).join(", ")
646+ fontSize: "x-small"
647+ }
648+
649+ Label {
650+ id: stateName
651+ width: parent.width
652+ wrapMode: Text.WordWrap
653+ visible: text.length > 0
654+ text: securityStatus.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrStateOrProvinceName).join(", ")
655+ fontSize: "x-small"
656+ }
657+
658+ Label {
659+ id: countryName
660+ width: parent.width
661+ wrapMode: Text.WordWrap
662+ visible: text.length > 0
663+ text: securityStatus.certificate.getSubjectInfo(Oxide.SslCertificate.PrincipalAttrCountryName).join(", ")
664+ fontSize: "x-small"
665+ }
666+
667+ Item {
668+ height: units.gu(1.5)
669+ width: parent.width
670+ }
671+ }
672+
673+ MouseArea {
674+ anchors.fill: parent
675+ onClicked: PopupUtils.close(certificatePopover)
676+ }
677+}
678
679=== added file 'pay-ui/app/payui.qml'
680--- pay-ui/app/payui.qml 1970-01-01 00:00:00 +0000
681+++ pay-ui/app/payui.qml 2016-03-11 14:49:24 +0000
682@@ -0,0 +1,459 @@
683+/*
684+ * Copyright 2014-2016 Canonical Ltd.
685+ *
686+ * This program is free software; you can redistribute it and/or modify
687+ * it under the terms of the GNU General Public License as published by
688+ * the Free Software Foundation; version 3.
689+ *
690+ * This program is distributed in the hope that it will be useful,
691+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
692+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
693+ * GNU General Public License for more details.
694+ *
695+ * You should have received a copy of the GNU General Public License
696+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
697+ */
698+
699+import QtQuick 2.0
700+import QtQuick.LocalStorage 2.0
701+import Ubuntu.Components 1.1
702+import Ubuntu.Components.Popups 0.1
703+import Ubuntu.OnlineAccounts 0.1
704+import Ubuntu.OnlineAccounts.Client 0.1
705+import payui 0.1
706+import "ui"
707+
708+/*!
709+ states:
710+ - add-payment
711+ - buy-interaction
712+ - checkout
713+ - online-accounts
714+ - error
715+*/
716+
717+MainView {
718+ id: mainView
719+ // objectName for functional testing purposes (autopilot-qt5)
720+ objectName: "payui"
721+
722+ // NOTE: Must match the gettext domain for translations.
723+ applicationName: "pay-service"
724+
725+ /*
726+ This property enables the application to change orientation
727+ when the device is rotated. The default is false.
728+ */
729+ // automaticOrientation: true
730+
731+ width: units.gu(100)
732+ height: units.gu(75)
733+
734+ useDeprecatedToolbar: false
735+
736+ property bool loading: true
737+ property bool purchasing: false
738+ property bool recentLogin: false
739+ property bool cancellable: true
740+ property string suggestedCurrency: "USD"
741+
742+ backgroundColor: "transparent"
743+
744+ onStateChanged: {
745+ mainView.backgroundColor = "white";
746+ }
747+
748+ AccountServiceModel {
749+ id: accounts
750+ provider: "ubuntuone"
751+ }
752+
753+ Setup {
754+ id: setup
755+ applicationId: "pay-service"
756+ providerId: "ubuntuone"
757+
758+ onFinished: {
759+ mainView.recentLogin = true;
760+ mainView.showLoading();
761+ purchase.checkCredentials();
762+ }
763+ }
764+
765+ Purchase {
766+ id: purchase
767+
768+ onItemDetailsObtained: {
769+ suggestedCurrency = currency;
770+ checkout.itemIcon = icon;
771+ checkout.itemTitle = title;
772+ checkout.itemSubtitle = publisher;
773+ checkout.price = formatted_price;
774+ }
775+
776+ onPaymentTypesObtained: {
777+ mainView.recentLogin = mainView.recentCredentials();
778+ checkout.beforeTimeout = mainView.recentLogin;
779+
780+ // Check for selected payment, and keep it selected if so.
781+ if (checkout.hasSelectedPayment) {
782+ for (var i=0; i < payments.length; i++) {
783+ if (payments[i].paymentId == checkout.paymentId &&
784+ payments[i].backendId == checkout.backendId) {
785+ payments[i].preferred = true;
786+ } else {
787+ payments[i].preferred = false;
788+ }
789+ }
790+ }
791+ checkout.model = payments;
792+ checkout.hasPayments = payments.length != 0;
793+ checkout.setSelectedItem();
794+
795+ mainView.state = "checkout";
796+ pageStack.push(checkout);
797+
798+ hideLoading();
799+ }
800+
801+ onNoPreferredPaymentMethod: {
802+ checkout.hasPreferredPayment = false;
803+ var values = mainView.getLastPayment();
804+ var backendid = values[0];
805+ var paymentid = values[1];
806+ if (backendid != "" && paymentid != "") {
807+ checkout.hasStoredPayment = true;
808+ }
809+ }
810+
811+ onPasswordValid: {
812+ hideLoading();
813+ // Reset password and otp to not keep them in memory.
814+ checkout.password = "";
815+ checkout.otp = "";
816+ }
817+
818+ onBuyItemFailed: {
819+ hideLoading();
820+ purchaseErrorDialog.open = true;
821+ }
822+
823+ onBuyItemSucceeded: {
824+ purchase.quitSuccess();
825+ }
826+
827+ onBuyInterationRequired: {
828+ mainView.purchasing = false;
829+ webkit.title = i18n.tr("Finish Purchase");
830+ webkit.url = url;
831+ mainView.state = "buy-interaction";
832+ pageStack.push(webkit);
833+ }
834+
835+ onError: {
836+ hideLoading();
837+ serverErrorDialog.open = true;
838+ }
839+
840+ onAuthenticationError: {
841+ mainView.recentLogin = false;
842+ if (pageStack.currentPage == checkout) {
843+ mainView.hideLoading();
844+ checkout.showErrorMessage(i18n.tr("Incorrect Password, please try again."));
845+ } else {
846+ mainView.state = "online-accounts";
847+ setup.exec();
848+ }
849+ }
850+
851+ onCredentialsFound: {
852+ mainView.recentLogin = mainView.recentCredentials();
853+ checkout.beforeTimeout = mainView.recentLogin;
854+
855+ if (mainView.state == "online-accounts") {
856+ purchase.checkItemPurchased();
857+ } else if (mainView.state != "checkout" && !mainView.purchasing && mainView.state != "buy-interaction") {
858+ purchase.getPaymentTypes(suggestedCurrency);
859+ }
860+ purchase.getItemDetails();
861+ }
862+
863+ onItemNotPurchased: {
864+ purchase.getPaymentTypes(suggestedCurrency);
865+ }
866+
867+ onCredentialsNotFound: {
868+ mainView.recentLogin = false;
869+ if (mainView.state == "online-accounts") {
870+ purchase.quitCancel();
871+ } else {
872+ mainView.state = "online-accounts";
873+ setup.exec();
874+ }
875+ }
876+
877+ onLoginError: {
878+ mainView.recentLogin = false;
879+ mainView.hideLoading();
880+ checkout.showErrorMessage(message);
881+ }
882+
883+ onTwoFactorAuthRequired: {
884+ mainView.hideLoading();
885+ checkout.showTwoFactor();
886+ }
887+
888+ onCertificateFound: {
889+ checkout.certificate = cert
890+ }
891+ }
892+
893+ function showLoading() {
894+ mainView.loading = true;
895+ PopupUtils.open(loadingDialogContainer);
896+ }
897+
898+ function hideLoading() {
899+ mainView.purchasing = false;
900+ mainView.loading = false;
901+ mainView.cancellable = true;
902+ }
903+
904+ function createDB() {
905+ var db = LocalStorage.openDatabaseSync("PayUI", "1.0", "PayUI Credentials Date", 100);
906+ db.transaction(
907+ function(tx) {
908+ // Create the database if it doesn't already exist
909+ tx.executeSql('CREATE TABLE IF NOT EXISTS PayUIPayment(backendid TEXT, paymentid TEXT)');
910+ }
911+ )
912+ }
913+
914+ function recentCredentials() {
915+ var valid = false;
916+ var date = purchase.getTokenUpdated();
917+ var currentDate = new Date();
918+ var msec = currentDate - date;
919+ var mm = Math.floor(msec / 1000 / 60);
920+ if (mm < 15) {
921+ valid = true;
922+ }
923+ return valid;
924+ }
925+
926+ function getLastPayment() {
927+ var backendid = "";
928+ var paymentid = "";
929+ var db = LocalStorage.openDatabaseSync("PayUI", "1.0", "PayUI Credentials Date", 100);
930+ db.transaction(
931+ function(tx) {
932+ var rs = tx.executeSql('SELECT * FROM PayUIPayment');
933+ if (rs.rows.length > 0) {
934+ backendid = rs.rows.item(0).backendid;
935+ paymentid = rs.rows.item(0).paymentid;
936+ }
937+ }
938+ )
939+ return [backendid, paymentid];
940+ }
941+
942+ function updatePayment(backendid, paymentid) {
943+ var db = LocalStorage.openDatabaseSync("PayUI", "1.0", "PayUI Credentials Date", 100);
944+ db.transaction(
945+ function(tx) {
946+ var rs = tx.executeSql('SELECT * FROM PayUIPayment');
947+ if (rs.rows.length > 0) {
948+ tx.executeSql('UPDATE PayUIPayment SET backendid = "' + backendid + '", paymentid = "' + paymentid + '"');
949+ } else {
950+ tx.executeSql('INSERT INTO PayUIPayment VALUES(?, ?)', [backendid, paymentid]);
951+ }
952+ }
953+ )
954+ }
955+
956+ ErrorDialog {
957+ id: purchaseErrorDialog
958+ title: i18n.tr("Purchase failed")
959+ message: i18n.tr("The purchase couldn't be completed.")
960+
961+ onRetry: {
962+ mainView.state = "error";
963+ mainView.showLoading();
964+ purchase.getItemDetails();
965+ }
966+
967+ onClose: {
968+ purchase.quitCancel();
969+ }
970+ }
971+
972+ ErrorDialog {
973+ id: serverErrorDialog
974+ title: i18n.tr("Error contacting the server")
975+ message: i18n.tr("Do you want to try again?")
976+
977+ onRetry: {
978+ mainView.state = "error";
979+ mainView.showLoading();
980+ purchase.getItemDetails();
981+ }
982+
983+ onClose: {
984+ purchase.quitCancel();
985+ }
986+ }
987+
988+ ErrorDialog {
989+ id: creditCardErrorDialog
990+ title: i18n.tr("Adding Credit Card failed")
991+ message: i18n.tr("Do you want to try again?")
992+
993+ onRetry: {
994+ mainView.state = "error";
995+ mainView.showLoading();
996+ purchase.getItemDetails();
997+ }
998+
999+ onClose: {
1000+ purchase.quitCancel();
1001+ }
1002+ }
1003+
1004+ Component {
1005+ id: loadingDialogContainer
1006+
1007+ Dialog {
1008+ id: loadingDialog
1009+ title: mainView.purchasing ? i18n.tr("Processing Purchase") : i18n.tr("Loading")
1010+ text: i18n.tr("Please wait…")
1011+ ActivityIndicator {
1012+ running: mainView.loading ? true : false
1013+ width: parent.width
1014+
1015+ onRunningChanged: {
1016+ if(!running) {
1017+ PopupUtils.close(loadingDialog);
1018+ }
1019+ }
1020+ }
1021+
1022+ Button {
1023+ objectName: "buttonCancelLoading"
1024+ text: i18n.tr("Cancel")
1025+ color: UbuntuColors.orange
1026+ visible: mainView.cancellable
1027+ onClicked: {
1028+ PopupUtils.close(loadingDialog);
1029+ purchase.quitCancel();
1030+ }
1031+ }
1032+ }
1033+ }
1034+
1035+ PageStack {
1036+ id: pageStack
1037+ objectName: "pageStack"
1038+
1039+ Component.onCompleted: {
1040+ showLoading();
1041+ mainView.createDB();
1042+ purchase.checkCredentials();
1043+ }
1044+
1045+ onCurrentPageChanged: {
1046+ if (pageStack.currentPage == checkout) {
1047+ mainView.state = "checkout";
1048+ }
1049+ }
1050+
1051+ CheckoutPage {
1052+ id: checkout
1053+ objectName: "pageCheckout"
1054+ visible: false
1055+ account: accounts
1056+
1057+ onCancel: {
1058+ purchase.quitCancel();
1059+ }
1060+
1061+ onBuy: {
1062+ mainView.recentLogin = mainView.recentCredentials();
1063+ checkout.beforeTimeout = mainView.recentLogin;
1064+
1065+ // Pass it on.
1066+ checkout.hasSelectedPayment = true;
1067+ checkout.backendId = backendId;
1068+ checkout.paymentId = paymentId;
1069+
1070+ mainView.purchasing = true;
1071+ mainView.cancellable = false;
1072+ showLoading();
1073+ if (!checkout.hasPreferredPayment) {
1074+ mainView.updatePayment(backendId, paymentId);
1075+ }
1076+
1077+ if (mainView.recentLogin) {
1078+ purchase.buyItem(email, "", "", suggestedCurrency, paymentId, backendId, mainView.recentLogin);
1079+ } else {
1080+ purchase.buyItem(email, password, otp, suggestedCurrency, paymentId, backendId, mainView.recentLogin);
1081+ }
1082+ }
1083+
1084+ onAddCreditCard: {
1085+ webkit.title = i18n.tr("Add Payment");
1086+ webkit.url = purchase.getAddPaymentUrl(suggestedCurrency);
1087+ mainView.state = "add-payment";
1088+ pageStack.push(webkit);
1089+ }
1090+ }
1091+
1092+ UbuntuPurchaseWebkit {
1093+ id: webkit
1094+ visible: false
1095+
1096+ onPurchaseFailed: {
1097+ pageStack.pop();
1098+ hideLoading();
1099+ purchaseErrorDialog.open = true;
1100+ }
1101+
1102+ onPurchaseCancelled: {
1103+ hideLoading();
1104+ if (mainView.state == "add-payment") {
1105+ mainView.state = "checkout";
1106+ pageStack.pop();
1107+ } else {
1108+ purchase.quitCancel();
1109+ }
1110+ }
1111+
1112+ onPurchaseSucceeded: {
1113+ if (mainView.state == "add-payment") {
1114+ showLoading();
1115+ purchase.getPaymentTypes(suggestedCurrency);
1116+ } else {
1117+ purchase.quitSuccess();
1118+ }
1119+ }
1120+
1121+ onLoading: {
1122+ if (value) {
1123+ showLoading();
1124+ } else {
1125+ hideLoading();
1126+ }
1127+ }
1128+ }
1129+ }
1130+
1131+ Rectangle {
1132+ id: lockIconPlace
1133+ width: units.gu(7)
1134+ height: units.gu(7)
1135+ anchors {
1136+ right: parent.right
1137+ top: parent.top
1138+ }
1139+ visible: false
1140+ }
1141+}
1142
1143=== added directory 'pay-ui/app/tests'
1144=== added directory 'pay-ui/app/tests/unit'
1145=== added directory 'pay-ui/app/tests/unit/js'
1146=== added file 'pay-ui/app/tests/unit/js/unit_test.js'
1147--- pay-ui/app/tests/unit/js/unit_test.js 1970-01-01 00:00:00 +0000
1148+++ pay-ui/app/tests/unit/js/unit_test.js 2016-03-11 14:49:24 +0000
1149@@ -0,0 +1,17 @@
1150+.pragma library
1151+
1152+// Find an object with the given name in the children tree of "obj"
1153+function findChild(obj,objectName) {
1154+ var childs = new Array(0);
1155+ childs.push(obj)
1156+ while (childs.length > 0) {
1157+ if (childs[0].objectName == objectName) {
1158+ return childs[0]
1159+ }
1160+ for (var i in childs[0].children) {
1161+ childs.push(childs[0].children[i])
1162+ }
1163+ childs.splice(0, 1);
1164+ }
1165+ return undefined;
1166+}
1167
1168=== added file 'pay-ui/app/tests/unit/tst_checkoutpage.qml'
1169--- pay-ui/app/tests/unit/tst_checkoutpage.qml 1970-01-01 00:00:00 +0000
1170+++ pay-ui/app/tests/unit/tst_checkoutpage.qml 2016-03-11 14:49:24 +0000
1171@@ -0,0 +1,73 @@
1172+import QtQuick 2.0
1173+import QtTest 1.0
1174+import Ubuntu.Components 0.1
1175+import "../../ui"
1176+import "js/unit_test.js" as UT
1177+
1178+// See more details @ http://qt-project.org/doc/qt-5.0/qtquick/qml-testcase.html
1179+
1180+// Execute tests with:
1181+// qmltestrunner
1182+
1183+Item {
1184+ id: root
1185+
1186+ property string title: "My App"
1187+ property string subtitle: "My App Subtitle"
1188+ property string price: "$ 1.99"
1189+ property string ubuntuid: "mail@mail.com"
1190+ property bool called: false
1191+
1192+ // The objects
1193+ CheckoutPage {
1194+ id: checkoutPage
1195+ itemTitle: root.title
1196+ itemSubtitle: root.subtitle
1197+ price: root.price
1198+ ubuntuID: root.ubuntuid
1199+
1200+ onCancel: {
1201+ root.called = true;
1202+ }
1203+
1204+ onBuy: {
1205+ root.called = true;
1206+ }
1207+ }
1208+
1209+ TestCase {
1210+ name: "CheckoutPage"
1211+
1212+ function init() {
1213+ console.debug("Cleaning vars");
1214+ root.called = false;
1215+ }
1216+
1217+ function test_uiTexts() {
1218+ var titleLabel = UT.findChild(checkoutPage, "titleLabel");
1219+ var subtitleLabel = UT.findChild(checkoutPage, "subtitleLabel");
1220+ var priceLabel = UT.findChild(checkoutPage, "priceLabel");
1221+ var ubuntuIdLabel = UT.findChild(checkoutPage, "ubuntuIdLabel");
1222+ var ubuntuidExpected = "Ubuntu ID: " + root.ubuntuid
1223+
1224+ compare(titleLabel.text, root.title);
1225+ compare(subtitleLabel.text, root.subtitle);
1226+ compare(priceLabel.text, root.price);
1227+ compare(ubuntuIdLabel.text, ubuntuidExpected);
1228+ }
1229+
1230+ function test_cancelPressed() {
1231+ compare(root.called, false);
1232+ var cancelButton = UT.findChild(checkoutPage, "cancelButton");
1233+ cancelButton.clicked();
1234+ compare(root.called, true);
1235+ }
1236+
1237+ function test_buyPressed() {
1238+ compare(root.called, false);
1239+ var buyButton = UT.findChild(checkoutPage, "buyButton");
1240+ buyButton.clicked();
1241+ compare(root.called, true);
1242+ }
1243+ }
1244+}
1245
1246=== added file 'pay-ui/app/tests/unit/tst_purchasewebkit.qml'
1247--- pay-ui/app/tests/unit/tst_purchasewebkit.qml 1970-01-01 00:00:00 +0000
1248+++ pay-ui/app/tests/unit/tst_purchasewebkit.qml 2016-03-11 14:49:24 +0000
1249@@ -0,0 +1,57 @@
1250+import QtQuick 2.0
1251+import QtTest 1.0
1252+import Ubuntu.Components 0.1
1253+import "../../ui"
1254+
1255+// See more details @ http://qt-project.org/doc/qt-5.0/qtquick/qml-testcase.html
1256+
1257+// Execute tests with:
1258+// qmltestrunner
1259+
1260+Item {
1261+ id: root
1262+
1263+ property bool succeeded: false
1264+ property bool failed: false
1265+
1266+ // The objects
1267+ UbuntuPurchaseWebkit {
1268+ id: purchaseWebkit
1269+
1270+ onPurchaseCanceled: {
1271+ root.failed = true;
1272+ }
1273+
1274+ onPurchaseSucceeded: {
1275+ root.succeeded = true;
1276+ }
1277+ }
1278+
1279+ TestCase {
1280+ name: "UbuntuPurchaseWebkitPage"
1281+
1282+ function init() {
1283+ console.debug("Cleaning vars");
1284+ root.succeeded = false;
1285+ root.failed = false;
1286+ }
1287+
1288+ function test_normalNavigation() {
1289+ purchaseWebkit.url = "http://fakepage.com";
1290+ compare(root.failed, false);
1291+ compare(root.succeeded, false);
1292+ }
1293+
1294+ function test_succeeded() {
1295+ purchaseWebkit.url = "https://sc.staging.ubuntu.com/click/succeeded";
1296+ compare(root.failed, false);
1297+ compare(root.succeeded, true);
1298+ }
1299+
1300+ function test_failed() {
1301+ purchaseWebkit.url = "https://sc.staging.ubuntu.com/click/succeeded";
1302+ compare(root.failed, true);
1303+ compare(root.succeeded, false);
1304+ }
1305+ }
1306+}
1307
1308=== added directory 'pay-ui/app/ui'
1309=== added file 'pay-ui/app/ui/CMakeLists.txt'
1310--- pay-ui/app/ui/CMakeLists.txt 1970-01-01 00:00:00 +0000
1311+++ pay-ui/app/ui/CMakeLists.txt 2016-03-11 14:49:24 +0000
1312@@ -0,0 +1,6 @@
1313+file(GLOB UI_QML_JS_FILES *.qml *.js)
1314+
1315+# make the files visible in the qtcreator tree
1316+add_custom_target(payui_ui_QMlFiles ALL SOURCES ${UI_QML_JS_FILES})
1317+
1318+install(FILES ${UI_QML_JS_FILES} DESTINATION ${PAYUI_DIR}/ui)
1319
1320=== added file 'pay-ui/app/ui/CheckoutPage.qml'
1321--- pay-ui/app/ui/CheckoutPage.qml 1970-01-01 00:00:00 +0000
1322+++ pay-ui/app/ui/CheckoutPage.qml 2016-03-11 14:49:24 +0000
1323@@ -0,0 +1,404 @@
1324+/*
1325+ * Copyright 2014 Canonical Ltd.
1326+ *
1327+ * This program is free software; you can redistribute it and/or modify
1328+ * it under the terms of the GNU General Public License as published by
1329+ * the Free Software Foundation; version 3.
1330+ *
1331+ * This program is distributed in the hope that it will be useful,
1332+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1333+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1334+ * GNU General Public License for more details.
1335+ *
1336+ * You should have received a copy of the GNU General Public License
1337+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1338+ */
1339+
1340+import QtQuick 2.0
1341+import Ubuntu.Components 1.1
1342+import Ubuntu.Components.ListItems 0.1 as ListItem
1343+import Ubuntu.Components.Popups 0.1
1344+import payui 0.1 as Oxide
1345+import "../components"
1346+
1347+Page {
1348+ id: pageCheckout
1349+
1350+ title: i18n.tr("Payment")
1351+
1352+ property int keyboardSize: Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0
1353+ property alias selectedItem: paymentTypes.selectedIndex
1354+
1355+ property alias itemIcon: iconImage.source
1356+ property alias itemTitle: titleLabel.text
1357+ property alias itemSubtitle: subtitleLabel.text
1358+ property alias price: priceLabel.text
1359+ property alias account: accountView.model
1360+ property alias model: paymentTypes.model
1361+ property alias password: passwordField.text
1362+ property alias otp: twoFactorField.text
1363+ property alias certificate: otherSecurityStatus.certificate
1364+ property alias securityStatus: otherSecurityStatus
1365+
1366+ property bool hasPayments: false
1367+ property bool hasPreferredPayment: true
1368+ property bool hasStoredPayment: false
1369+ property bool beforeTimeout: false
1370+
1371+ property bool hasSelectedPayment: false
1372+ property string backendId: ""
1373+ property string paymentId: ""
1374+
1375+ signal cancel
1376+ signal buy(string email, string password, string otp, string paymentId, string backendId)
1377+ signal addCreditCard
1378+
1379+ function launchPurchase() {
1380+ var pay = paymentTypes.model[pageCheckout.selectedItem];
1381+ var email = accountView.currentItem.email;
1382+ pageCheckout.buy(email, password, otp, pay.paymentId, pay.backendId);
1383+ }
1384+
1385+ function showErrorMessage(message) {
1386+ errorLabel.text = message;
1387+ errorLabel.visible = true;
1388+ }
1389+
1390+ function showTwoFactor() {
1391+ errorLabel.visible = false;
1392+ twoFactorUI.visible = true;
1393+ }
1394+
1395+ function setSelectedItem() {
1396+ for (var i=0; i < pageCheckout.model.length; i++) {
1397+ if (pageCheckout.model[i].preferred) {
1398+ selectedItem = i;
1399+ }
1400+ }
1401+ }
1402+
1403+ QtObject {
1404+ id: otherSecurityStatus
1405+ property int securityLevel: certificate == null ? Oxide.SecurityStatus.SecurityLevelNone : Oxide.SecurityStatus.SecurityLevelSecure
1406+ property var certificate: null
1407+ }
1408+
1409+ head.actions:[
1410+ Action {
1411+ id: lockAction
1412+ iconName: pageCheckout.securityStatus.securityLevel == Oxide.SecurityStatus.SecurityLevelSecure ? "lock" : "security-alert"
1413+ onTriggered: {
1414+ PopupUtils.open(popoverComponent, lockIconPlace, {"securityStatus": pageCheckout.securityStatus})
1415+ }
1416+ }
1417+ ]
1418+
1419+ Component {
1420+ id: popoverComponent
1421+
1422+ SecurityCertificatePopover {
1423+ id: certPopover
1424+ securityStatus: null
1425+ }
1426+ }
1427+
1428+ Flickable {
1429+ id: checkoutFlickable
1430+ anchors {
1431+ left: parent.left
1432+ right: parent.right
1433+ top: parent.top
1434+ }
1435+
1436+ contentHeight: contentItem.childrenRect.height + pageCheckout.keyboardSize
1437+
1438+ Item {
1439+ id: header
1440+ height: units.gu(8)
1441+ anchors {
1442+ left: parent.left
1443+ right: parent.right
1444+ top: parent.top
1445+ topMargin: units.gu(1)
1446+ }
1447+
1448+ UbuntuShape {
1449+ id: iconShape
1450+ objectName: "iconShape"
1451+ anchors {
1452+ top: parent.top
1453+ left: parent.left
1454+ margins: units.gu(1)
1455+ }
1456+ image: Image {
1457+ id: iconImage
1458+ objectName: "iconImage"
1459+ }
1460+ width: units.gu(6)
1461+ height: units.gu(6)
1462+ }
1463+
1464+ Column {
1465+ id: col
1466+ spacing: units.gu(0.5)
1467+ anchors {
1468+ left: iconShape.right
1469+ top: parent.top
1470+ right: priceLabel.left
1471+ bottom: parent.bottom
1472+ margins: units.gu(1)
1473+ }
1474+
1475+ Label {
1476+ id: titleLabel
1477+ objectName: "titleLabel"
1478+ fontSize: "medium"
1479+ anchors {
1480+ left: parent.left
1481+ right: parent.right
1482+ }
1483+ elide: Text.ElideRight
1484+ }
1485+ Label {
1486+ id: subtitleLabel
1487+ objectName: "subtitleLabel"
1488+ fontSize: "small"
1489+ anchors {
1490+ left: parent.left
1491+ right: parent.right
1492+ }
1493+ elide: Text.ElideRight
1494+ }
1495+ }
1496+
1497+ Label {
1498+ id: priceLabel
1499+ objectName: "priceLabel"
1500+ font.bold: true
1501+ fontSize: "large"
1502+ verticalAlignment: Text.AlignVCenter
1503+
1504+ anchors {
1505+ right: parent.right
1506+ top: parent.top
1507+ bottom: parent.bottom
1508+ rightMargin: units.gu(2)
1509+ }
1510+ }
1511+ }
1512+
1513+ Rectangle {
1514+ id: separator
1515+ height: units.dp(1)
1516+ color: "#d5d5d5"
1517+ anchors {
1518+ left: parent.left
1519+ right: parent.right
1520+ top: header.bottom
1521+ rightMargin: units.gu(2)
1522+ leftMargin: units.gu(2)
1523+ topMargin: units.gu(1)
1524+ }
1525+ }
1526+
1527+ ListView {
1528+ id: accountView
1529+ anchors {
1530+ left: parent.left
1531+ right: parent.right
1532+ top: separator.bottom
1533+ leftMargin: units.gu(2)
1534+ rightMargin: units.gu(2)
1535+ topMargin: units.gu(2)
1536+ }
1537+ height: units.gu(2)
1538+ enabled: false
1539+ delegate: Label {
1540+ id: ubuntuIdLabel
1541+ objectName: "ubuntuIdLabel"
1542+ text: model.displayName
1543+ elide: Text.ElideRight
1544+ property string email: model.displayName
1545+ }
1546+ }
1547+
1548+ TextField {
1549+ id: passwordField
1550+ objectName: "passwordField"
1551+ placeholderText: i18n.tr("Enter your Ubuntu One password")
1552+ echoMode: TextInput.Password
1553+ visible: !pageCheckout.beforeTimeout
1554+ anchors {
1555+ left: parent.left
1556+ right: parent.right
1557+ top: accountView.bottom
1558+ margins: units.gu(2)
1559+ }
1560+
1561+ Keys.onReturnPressed: launchPurchase();
1562+ }
1563+
1564+ Label {
1565+ id: errorLabel
1566+ objectName: "errorLabel"
1567+ color: "red"
1568+ text: ""
1569+ wrapMode: Text.WordWrap
1570+ visible: false
1571+ anchors {
1572+ left: parent.left
1573+ right: parent.right
1574+ top: passwordField.bottom
1575+ margins: units.gu(2)
1576+ }
1577+ }
1578+
1579+ Column {
1580+ id: twoFactorUI
1581+ spacing: units.gu(2)
1582+ anchors {
1583+ left: parent.left
1584+ right: parent.right
1585+ top: errorLabel.visible ? errorLabel.bottom : passwordField.bottom
1586+ margins: units.gu(2)
1587+ }
1588+ visible: false
1589+
1590+ Label {
1591+ id: twoFactorLabel
1592+ objectName: "twoFactorLabel"
1593+ color: "black"
1594+ text: i18n.tr("Type your verification code:")
1595+ wrapMode: Text.WordWrap
1596+ anchors {
1597+ left: parent.left
1598+ right: parent.right
1599+ }
1600+ }
1601+
1602+ TextField {
1603+ id: twoFactorField
1604+ objectName: "twoFactorField"
1605+ placeholderText: i18n.tr("2-factor device code")
1606+ inputMethodHints: Qt.ImhDigitsOnly
1607+ anchors {
1608+ left: parent.left
1609+ right: parent.right
1610+ }
1611+
1612+ Keys.onReturnPressed: launchPurchase();
1613+ }
1614+ }
1615+
1616+ Rectangle {
1617+ id: paymentSep
1618+ height: units.dp(1)
1619+ color: "#d5d5d5"
1620+ anchors {
1621+ left: parent.left
1622+ right: parent.right
1623+ top: twoFactorUI.visible ? twoFactorUI.bottom : (errorLabel.visible ? errorLabel.bottom : (passwordField.visible ? passwordField.bottom : accountView.bottom))
1624+ rightMargin: units.gu(2)
1625+ leftMargin: units.gu(2)
1626+ topMargin: units.gu(2)
1627+ }
1628+ }
1629+
1630+ OptionSelector {
1631+ id: paymentTypes
1632+ objectName: "paymentTypes"
1633+ anchors {
1634+ left: parent.left
1635+ right: parent.right
1636+ top: paymentSep.bottom
1637+ margins: units.gu(2)
1638+ }
1639+ containerHeight: units.gu(24)
1640+ expanded: false
1641+ delegate: OptionSelectorDelegate {
1642+ Item {
1643+ anchors.fill: parent
1644+ Column {
1645+ anchors {
1646+ fill: parent
1647+ leftMargin: units.gu(2)
1648+ topMargin: units.gu(2)
1649+ rightMargin: units.gu(5)
1650+ }
1651+ spacing: units.gu(0.25)
1652+
1653+ Label {
1654+ text: modelData.name
1655+ elide: Text.ElideLeft
1656+ anchors {
1657+ left: parent.left
1658+ right: parent.right
1659+ }
1660+ fontSize: "small"
1661+ }
1662+ Label {
1663+ text: modelData.description
1664+ elide: Text.ElideLeft
1665+ anchors {
1666+ left: parent.left
1667+ right: parent.right
1668+ }
1669+ fontSize: "x-small"
1670+ }
1671+ }
1672+ }
1673+
1674+ height: units.gu(8)
1675+ }
1676+ }
1677+
1678+ Row {
1679+ id: rowButtons
1680+ anchors {
1681+ left: parent.left
1682+ right: parent.right
1683+ top: paymentTypes.bottom
1684+ margins: units.gu(4)
1685+ }
1686+
1687+ spacing: units.gu(2)
1688+ property int buttonsWidth: (width / 2) - (spacing / 2)
1689+
1690+ Button {
1691+ id: cancelButton
1692+ objectName: "cancelButton"
1693+ text: i18n.tr("Cancel")
1694+ width: parent.buttonsWidth
1695+ color: "#797979"
1696+
1697+ onClicked: pageCheckout.cancel();
1698+ }
1699+ Button {
1700+ id: buyButton
1701+ objectName: "buyButton"
1702+ text: i18n.tr("Buy Now")
1703+ color: UbuntuColors.orange
1704+ width: parent.buttonsWidth
1705+
1706+ onClicked: {
1707+ launchPurchase();
1708+ }
1709+ }
1710+ }
1711+
1712+ Label {
1713+ id: addCreditCardLabel
1714+ objectName: "addCreditCardLabel"
1715+ textFormat: Text.RichText
1716+ text: '<a href="#"><span style="color: #797979;">%1</span></a>'.arg(i18n.tr("Add credit/debit card"))
1717+ anchors {
1718+ left: parent.left
1719+ right: parent.right
1720+ top: rowButtons.bottom
1721+ topMargin: units.gu(6)
1722+ }
1723+ horizontalAlignment: Text.AlignHCenter
1724+ onLinkActivated: pageCheckout.addCreditCard();
1725+ }
1726+ }
1727+}
1728
1729=== added file 'pay-ui/app/ui/ErrorDialog.qml'
1730--- pay-ui/app/ui/ErrorDialog.qml 1970-01-01 00:00:00 +0000
1731+++ pay-ui/app/ui/ErrorDialog.qml 2016-03-11 14:49:24 +0000
1732@@ -0,0 +1,67 @@
1733+/*
1734+ * Copyright 2014 Canonical Ltd.
1735+ *
1736+ * This program is free software; you can redistribute it and/or modify
1737+ * it under the terms of the GNU General Public License as published by
1738+ * the Free Software Foundation; version 3.
1739+ *
1740+ * This program is distributed in the hope that it will be useful,
1741+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1742+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1743+ * GNU General Public License for more details.
1744+ *
1745+ * You should have received a copy of the GNU General Public License
1746+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1747+ */
1748+
1749+import QtQuick 2.0
1750+import Ubuntu.Components 0.1
1751+import Ubuntu.Components.Popups 0.1
1752+
1753+Item {
1754+ id: dialogErrorItem
1755+ property string title: ""
1756+ property string message: ""
1757+ property bool open: false
1758+
1759+ signal retry
1760+ signal close
1761+
1762+ onOpenChanged: {
1763+ if (dialogErrorItem.open) {
1764+ PopupUtils.open(errorDialogContainer);
1765+ }
1766+ }
1767+
1768+ Component {
1769+ id: errorDialogContainer
1770+
1771+ Dialog {
1772+ id: errorDialog
1773+ title: dialogErrorItem.title
1774+ text: dialogErrorItem.message
1775+
1776+ Button {
1777+ text: i18n.tr("Retry")
1778+ objectName: "retryErrorButton"
1779+ color: UbuntuColors.orange
1780+ onClicked: {
1781+ dialogErrorItem.retry();
1782+ dialogErrorItem.open = false;
1783+ PopupUtils.close(errorDialog);
1784+ }
1785+ }
1786+ Button {
1787+ text: i18n.tr("Close")
1788+ objectName: "closeErrorButton"
1789+ color: UbuntuColors.coolGrey
1790+ onClicked: {
1791+ dialogErrorItem.close();
1792+ dialogErrorItem.open = false;
1793+ PopupUtils.close(errorDialog);
1794+ }
1795+ }
1796+ }
1797+ }
1798+
1799+}
1800
1801=== added file 'pay-ui/app/ui/UbuntuPurchaseWebkit.qml'
1802--- pay-ui/app/ui/UbuntuPurchaseWebkit.qml 1970-01-01 00:00:00 +0000
1803+++ pay-ui/app/ui/UbuntuPurchaseWebkit.qml 2016-03-11 14:49:24 +0000
1804@@ -0,0 +1,107 @@
1805+/*
1806+ * Copyright 2013-2014 Canonical Ltd.
1807+ *
1808+ * This program is free software; you can redistribute it and/or modify
1809+ * it under the terms of the GNU General Public License as published by
1810+ * the Free Software Foundation; version 3.
1811+ *
1812+ * This program is distributed in the hope that it will be useful,
1813+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1814+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1815+ * GNU General Public License for more details.
1816+ *
1817+ * You should have received a copy of the GNU General Public License
1818+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1819+ */
1820+
1821+import QtQuick 2.0
1822+import Ubuntu.Components 1.1
1823+import Ubuntu.Components.Popups 0.1
1824+import Ubuntu.Web 0.2
1825+import "../components"
1826+
1827+
1828+Page {
1829+ id: pageWebkit
1830+ objectName: "pageWebkit"
1831+
1832+ signal purchaseSucceeded()
1833+ signal purchaseFailed()
1834+ signal purchaseCancelled()
1835+ signal loading(bool value)
1836+
1837+ property int keyboardSize: Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0
1838+ property alias url: webView.url
1839+ property var securityStatus: webView.securityStatus
1840+
1841+ function parseQuery(url) {
1842+ var argsParsed = {};
1843+ var vars = url.split('?');
1844+ if(vars.length == 1) {
1845+ return argsParsed;
1846+ }
1847+
1848+ var args = vars[1].split('&');
1849+ for (var i=0; i < args.length; i++) {
1850+ var arg = decodeURI(args[i]);
1851+ if (arg.indexOf('=') == -1) {
1852+ argsParsed[arg.trim()] = true;
1853+ } else {
1854+ var keyvalue = arg.split('=');
1855+ argsParsed[keyvalue[0].trim()] = keyvalue[1].trim();
1856+ }
1857+ }
1858+
1859+ return argsParsed;
1860+ }
1861+
1862+ head.actions:[
1863+ Action {
1864+ id: lockAction
1865+ iconName: pageWebkit.securityStatus.securityLevel ? "lock" : "security-alert"
1866+ onTriggered: {
1867+ PopupUtils.open(popoverComponent, lockIconPlace, {"securityStatus": pageWebkit.securityStatus})
1868+ }
1869+ }
1870+ ]
1871+
1872+ Component {
1873+ id: popoverComponent
1874+
1875+ SecurityCertificatePopover {
1876+ id: certPopover
1877+ securityStatus: null
1878+ }
1879+ }
1880+
1881+ WebView {
1882+ id: webView
1883+ objectName: "webView"
1884+ anchors.fill: parent
1885+ anchors.bottomMargin: pageWebkit.keyboardSize
1886+
1887+ // We need to specify the dialogs to use for JS dialogs here.
1888+ alertDialog: AlertDialog {}
1889+ confirmDialog: ConfirmDialog {}
1890+ promptDialog: PromptDialog {}
1891+ beforeUnloadDialog: BeforeUnloadDialog {}
1892+
1893+ onLoadingChanged: {
1894+ pageWebkit.loading(webView.loading);
1895+ }
1896+
1897+ onUrlChanged: {
1898+ var re_succeeded = new RegExp("/click/succeeded");
1899+ var re_failed = new RegExp("/click/failed");
1900+ var re_cancelled = new RegExp("/click/cancelled");
1901+
1902+ if (re_succeeded.test(webView.url)) {
1903+ pageWebkit.purchaseSucceeded();
1904+ } else if (re_failed.test(webView.url)) {
1905+ pageWebkit.purchaseFailed();
1906+ } else if (re_cancelled.test(webView.url)) {
1907+ pageWebkit.purchaseCancelled();
1908+ }
1909+ }
1910+ }
1911+}
1912
1913=== added directory 'pay-ui/backend'
1914=== added file 'pay-ui/backend/CMakeLists.txt'
1915--- pay-ui/backend/CMakeLists.txt 1970-01-01 00:00:00 +0000
1916+++ pay-ui/backend/CMakeLists.txt 2016-03-11 14:49:24 +0000
1917@@ -0,0 +1,49 @@
1918+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall")
1919+
1920+find_package(Qt5Core)
1921+find_package(Qt5Qml)
1922+find_package(Qt5Quick)
1923+
1924+include_directories(
1925+ ${CMAKE_CURRENT_SOURCE_DIR}
1926+)
1927+
1928+pkg_check_modules(UBUNTUONEAUTH REQUIRED ubuntuoneauth-2.0)
1929+add_definitions(${UBUNTUONEAUTH_CFLAGS} ${UBUNTUONEAUTH_CFLAGS_OTHER})
1930+
1931+set(
1932+ payuibackend_SRCS
1933+ modules/payui/backend.cpp
1934+ modules/payui/purchase.cpp
1935+ modules/payui/network.cpp
1936+ modules/payui/pay_info.cpp
1937+ modules/payui/credentials_service.cpp
1938+ modules/payui/certificateadapter.cpp
1939+ modules/payui/oxideconstants.cpp
1940+)
1941+set(PAYUI_BACKEND payuibackend)
1942+
1943+add_library(${PAYUI_BACKEND} SHARED
1944+ ${payuibackend_SRCS}
1945+)
1946+
1947+target_link_libraries(${PAYUI_BACKEND}
1948+ ${UBUNTUONEAUTH_LDFLAGS}
1949+)
1950+
1951+set_target_properties(${PAYUI_BACKEND} PROPERTIES
1952+ LIBRARY_OUTPUT_DIRECTORY payui)
1953+
1954+qt5_use_modules(${PAYUI_BACKEND} Gui Qml Quick Network DBus)
1955+
1956+# Copy qmldir file to build dir for running in QtCreator
1957+add_custom_target(payuibackend-qmldir ALL
1958+ COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/modules/payui/qmldir ${CMAKE_CURRENT_BINARY_DIR}/payui
1959+ DEPENDS ${QMLFILES}
1960+)
1961+
1962+# Install plugin file
1963+install(TARGETS ${PAYUI_BACKEND} DESTINATION ${QT_IMPORTS_DIR}/payui/)
1964+install(FILES modules/payui/qmldir DESTINATION ${QT_IMPORTS_DIR}/payui/)
1965+
1966+add_subdirectory (tests)
1967
1968=== added directory 'pay-ui/backend/modules'
1969=== added directory 'pay-ui/backend/modules/payui'
1970=== added file 'pay-ui/backend/modules/payui/backend.cpp'
1971--- pay-ui/backend/modules/payui/backend.cpp 1970-01-01 00:00:00 +0000
1972+++ pay-ui/backend/modules/payui/backend.cpp 2016-03-11 14:49:24 +0000
1973@@ -0,0 +1,23 @@
1974+#include <QtQml>
1975+#include <QtQml/QQmlContext>
1976+#include "backend.h"
1977+#include "purchase.h"
1978+#include "certificateadapter.h"
1979+#include "oxideconstants.h"
1980+#include "pay_info.h"
1981+
1982+void BackendPlugin::registerTypes(const char *uri)
1983+{
1984+ Q_ASSERT(uri == QLatin1String("payui"));
1985+
1986+ qmlRegisterType<UbuntuPurchase::Purchase>(uri, 0, 1, "Purchase");
1987+ qmlRegisterType<UbuntuPurchase::PayInfo>(uri, 0, 1, "PayInfo");
1988+ qmlRegisterType<CertificateAdapter>(uri, 0, 1, "CertificateAdapter");
1989+ qmlRegisterType<SecurityStatus>(uri, 0, 1, "SecurityStatus");
1990+ qmlRegisterType<SslCertificate>(uri, 0, 1, "SslCertificate");
1991+}
1992+
1993+void BackendPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
1994+{
1995+ QQmlExtensionPlugin::initializeEngine(engine, uri);
1996+}
1997
1998=== added file 'pay-ui/backend/modules/payui/backend.h'
1999--- pay-ui/backend/modules/payui/backend.h 1970-01-01 00:00:00 +0000
2000+++ pay-ui/backend/modules/payui/backend.h 2016-03-11 14:49:24 +0000
2001@@ -0,0 +1,33 @@
2002+#ifndef BACKEND_PLUGIN_H
2003+#define BACKEND_PLUGIN_H
2004+
2005+#include <QtQml/QQmlEngine>
2006+#include <QtQml/QQmlExtensionPlugin>
2007+
2008+/*
2009+ ----8<-----
2010+
2011+ import payui 0.1
2012+
2013+ Rectangle {
2014+ width: 200
2015+ height: 200
2016+
2017+ Purchase {
2018+ id: purchase
2019+ }
2020+ }
2021+
2022+ -----8<------
2023+*/
2024+class BackendPlugin : public QQmlExtensionPlugin
2025+{
2026+ Q_OBJECT
2027+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
2028+
2029+public:
2030+ void registerTypes(const char *uri);
2031+ void initializeEngine(QQmlEngine *engine, const char *uri);
2032+};
2033+#endif // BACKEND_PLUGIN_H
2034+
2035
2036=== added file 'pay-ui/backend/modules/payui/certificateadapter.cpp'
2037--- pay-ui/backend/modules/payui/certificateadapter.cpp 1970-01-01 00:00:00 +0000
2038+++ pay-ui/backend/modules/payui/certificateadapter.cpp 2016-03-11 14:49:24 +0000
2039@@ -0,0 +1,46 @@
2040+/*
2041+ * Copyright 2014 Canonical Ltd.
2042+ *
2043+ * This program is free software; you can redistribute it and/or
2044+ * modify it under the terms of version 3 of the GNU Lesser General Public
2045+ * License as published by the Free Software Foundation.
2046+ *
2047+ * This program is distributed in the hope that it will be useful,
2048+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2049+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
2050+ * General Public License for more details.
2051+ *
2052+ * You should have received a copy of the GNU Lesser General Public
2053+ * License along with this library; if not, write to the
2054+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
2055+ * Boston, MA 02110-1301, USA.
2056+ */
2057+
2058+#include <QStringList>
2059+#include "certificateadapter.h"
2060+
2061+CertificateAdapter::CertificateAdapter()
2062+{
2063+}
2064+
2065+CertificateAdapter::CertificateAdapter(const CertificateAdapter &other)
2066+{
2067+ m_cert = other.m_cert;
2068+}
2069+
2070+CertificateAdapter::~CertificateAdapter()
2071+{
2072+}
2073+
2074+CertificateAdapter::CertificateAdapter(QSslCertificate cert, QObject *parent) :
2075+ QObject(parent), m_cert(cert)
2076+{
2077+}
2078+
2079+QString CertificateAdapter::subjectDisplayName() {
2080+ return m_cert.subjectInfo(QSslCertificate::CommonName).join(", ");
2081+}
2082+
2083+QStringList CertificateAdapter::getSubjectInfo(int subject) {
2084+ return m_cert.subjectInfo((QSslCertificate::SubjectInfo)subject);
2085+}
2086
2087=== added file 'pay-ui/backend/modules/payui/certificateadapter.h'
2088--- pay-ui/backend/modules/payui/certificateadapter.h 1970-01-01 00:00:00 +0000
2089+++ pay-ui/backend/modules/payui/certificateadapter.h 2016-03-11 14:49:24 +0000
2090@@ -0,0 +1,51 @@
2091+/*
2092+ * Copyright 2014 Canonical Ltd.
2093+ *
2094+ * This program is free software; you can redistribute it and/or
2095+ * modify it under the terms of version 3 of the GNU Lesser General Public
2096+ * License as published by the Free Software Foundation.
2097+ *
2098+ * This program is distributed in the hope that it will be useful,
2099+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2100+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
2101+ * General Public License for more details.
2102+ *
2103+ * You should have received a copy of the GNU Lesser General Public
2104+ * License along with this library; if not, write to the
2105+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
2106+ * Boston, MA 02110-1301, USA.
2107+ */
2108+
2109+#ifndef CERTIFICATEADAPTER_H
2110+#define CERTIFICATEADAPTER_H
2111+
2112+#include <QObject>
2113+#include <QSslCertificate>
2114+#include <QMetaType>
2115+
2116+/*
2117+ * Adapt a QSSlCertificate to the interface that oxide uses for certs.
2118+ */
2119+class CertificateAdapter : public QObject
2120+{
2121+ Q_OBJECT
2122+ Q_PROPERTY(QString subjectDisplayName READ subjectDisplayName)
2123+public:
2124+ CertificateAdapter(QSslCertificate cert, QObject *parent = 0);
2125+ CertificateAdapter(const CertificateAdapter& other);
2126+ CertificateAdapter();
2127+ ~CertificateAdapter();
2128+
2129+signals:
2130+
2131+public slots:
2132+ QStringList getSubjectInfo(int subject);
2133+
2134+protected:
2135+ QString subjectDisplayName();
2136+ QSslCertificate m_cert;
2137+};
2138+
2139+Q_DECLARE_METATYPE(CertificateAdapter);
2140+
2141+#endif // CERTIFICATEADAPTER_H
2142
2143=== added file 'pay-ui/backend/modules/payui/credentials_service.cpp'
2144--- pay-ui/backend/modules/payui/credentials_service.cpp 1970-01-01 00:00:00 +0000
2145+++ pay-ui/backend/modules/payui/credentials_service.cpp 2016-03-11 14:49:24 +0000
2146@@ -0,0 +1,81 @@
2147+/*
2148+ * Copyright 2014 Canonical Ltd.
2149+ *
2150+ * This library is free software; you can redistribute it and/or
2151+ * modify it under the terms of version 3 of the GNU Lesser General Public
2152+ * License as published by the Free Software Foundation.
2153+ *
2154+ * This program is distributed in the hope that it will be useful,
2155+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2156+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
2157+ * General Public License for more details.
2158+ *
2159+ * You should have received a copy of the GNU Lesser General Public
2160+ * License along with this library; if not, write to the
2161+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
2162+ * Boston, MA 02110-1301, USA.
2163+ */
2164+
2165+#include "credentials_service.h"
2166+
2167+#include <errormessages.h>
2168+#include <token.h>
2169+#include <QProcessEnvironment>
2170+
2171+
2172+namespace UbuntuPurchase {
2173+
2174+bool useFakeCredentials()
2175+{
2176+ QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
2177+ QString getcreds = environment.value("GET_CREDENTIALS", "1");
2178+ return getcreds != "1";
2179+}
2180+
2181+CredentialsService::CredentialsService(QObject *parent) :
2182+ SSOService(parent)
2183+{
2184+ connect(this, &SSOService::requestFailed,
2185+ this, &CredentialsService::handleRequestFailed);
2186+}
2187+
2188+void CredentialsService::getCredentials()
2189+{
2190+ if (useFakeCredentials()) {
2191+ Token token("tokenkey", "tokensecret", "consumerkey", "consumersecret");
2192+ Q_EMIT credentialsFound(token);
2193+ } else {
2194+ if (!m_token.isValid()) {
2195+ SSOService::getCredentials();
2196+ } else {
2197+ Q_EMIT credentialsFound(m_token);
2198+ }
2199+ }
2200+}
2201+
2202+void CredentialsService::setCredentials(Token token)
2203+{
2204+ m_token = token;
2205+}
2206+
2207+void CredentialsService::login(const QString email,
2208+ const QString password,
2209+ const QString otp)
2210+{
2211+ if (m_token.isValid() || useFakeCredentials()) {
2212+ Q_EMIT SSOService::credentialsStored();
2213+ } else {
2214+ SSOService::login(email, password, otp);
2215+ }
2216+}
2217+
2218+void CredentialsService::handleRequestFailed(const ErrorResponse& error)
2219+{
2220+ if (error.httpStatus() == 0 || error.httpReason() == NO_HTTP_REASON) {
2221+ emit loginError("Network error, please try again.");
2222+ } else {
2223+ emit loginError(error.message());
2224+ }
2225+}
2226+
2227+}
2228
2229=== added file 'pay-ui/backend/modules/payui/credentials_service.h'
2230--- pay-ui/backend/modules/payui/credentials_service.h 1970-01-01 00:00:00 +0000
2231+++ pay-ui/backend/modules/payui/credentials_service.h 2016-03-11 14:49:24 +0000
2232@@ -0,0 +1,53 @@
2233+/*
2234+ * Copyright 2014 Canonical Ltd.
2235+ *
2236+ * This library is free software; you can redistribute it and/or
2237+ * modify it under the terms of version 3 of the GNU Lesser General Public
2238+ * License as published by the Free Software Foundation.
2239+ *
2240+ * This program is distributed in the hope that it will be useful,
2241+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2242+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
2243+ * General Public License for more details.
2244+ *
2245+ * You should have received a copy of the GNU Lesser General Public
2246+ * License along with this library; if not, write to the
2247+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
2248+ * Boston, MA 02110-1301, USA.
2249+ */
2250+
2251+#ifndef CREDENTIALS_SERVICE_H
2252+#define CREDENTIALS_SERVICE_H
2253+
2254+#include <QObject>
2255+#include <ssoservice.h>
2256+#include <token.h>
2257+
2258+using namespace UbuntuOne;
2259+
2260+namespace UbuntuPurchase {
2261+
2262+class CredentialsService : public SSOService
2263+{
2264+ Q_OBJECT
2265+
2266+public:
2267+ explicit CredentialsService(QObject *parent = 0);
2268+
2269+ void getCredentials();
2270+ void setCredentials(Token token);
2271+ void login(const QString email, const QString password,
2272+ const QString otp = QString());
2273+
2274+Q_SIGNALS:
2275+ void loginError(const QString& message);
2276+
2277+private Q_SLOTS:
2278+ void handleRequestFailed(const ErrorResponse& error);
2279+
2280+private:
2281+ Token m_token;
2282+};
2283+}
2284+
2285+#endif // CREDENTIALS_SERVICE_H
2286
2287=== added file 'pay-ui/backend/modules/payui/network.cpp'
2288--- pay-ui/backend/modules/payui/network.cpp 1970-01-01 00:00:00 +0000
2289+++ pay-ui/backend/modules/payui/network.cpp 2016-03-11 14:49:24 +0000
2290@@ -0,0 +1,512 @@
2291+/*
2292+ * Copyright 2014-2016 Canonical Ltd.
2293+ *
2294+ * This library is free software; you can redistribute it and/or
2295+ * modify it under the terms of version 3 of the GNU Lesser General Public
2296+ * License as published by the Free Software Foundation.
2297+ *
2298+ * This program is distributed in the hope that it will be useful,
2299+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2300+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
2301+ * General Public License for more details.
2302+ *
2303+ * You should have received a copy of the GNU Lesser General Public
2304+ * License along with this library; if not, write to the
2305+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
2306+ * Boston, MA 02110-1301, USA.
2307+ */
2308+
2309+#include "network.h"
2310+#include <QJsonDocument>
2311+#include <QJsonObject>
2312+#include <QJsonArray>
2313+#include <QJsonValue>
2314+#include <QByteArray>
2315+#include <QUrl>
2316+#include <QUrlQuery>
2317+#include <QDebug>
2318+#include <QFile>
2319+#include <QProcessEnvironment>
2320+#include <QDBusInterface>
2321+#include <QDBusReply>
2322+#include <QDBusConnection>
2323+
2324+#include "certificateadapter.h"
2325+
2326+#define PAY_PURCHASES_PATH "/purchases"
2327+#define PAY_PAYMENTMETHODS_PATH "/paymentmethods"
2328+#define PAY_PAYMENTMETHODS_ADD_PATH PAY_PAYMENTMETHODS_PATH + "/add"
2329+#define SUGGESTED_CURRENCY_HEADER_NAME "X-Suggested-Currency"
2330+
2331+#define DEVICE_ID_HEADER "X-Device-Id"
2332+
2333+#define PARTNER_ID_HEADER "X-Partner-ID"
2334+#define PARTNER_ID_FILE "/custom/partner-id"
2335+
2336+#define PREFERED_PAYMENT_TYPE "0"
2337+#define PAYMENT_TYPES "1"
2338+#define BUY_ITEM "2"
2339+#define ITEM_INFO "3"
2340+#define UPDATE_CREDENTIALS "4"
2341+#define CHECK_PASSWORD "5"
2342+#define CHECK_PURCHASED "6"
2343+
2344+#define BUY_COMPLETE "Complete"
2345+#define BUY_IN_PROGRESS "InProgress"
2346+
2347+namespace UbuntuPurchase {
2348+
2349+Network::Network(QObject *parent) :
2350+ QObject(parent),
2351+ m_nam(this),
2352+ m_service(this),
2353+ m_preferred(nullptr)
2354+{
2355+ connect(&m_nam, SIGNAL(finished(QNetworkReply*)),
2356+ this, SLOT(onReply(QNetworkReply*)));
2357+ // SSO SERVICE
2358+ connect(&m_service, &CredentialsService::credentialsFound,
2359+ this, &Network::handleCredentialsFound);
2360+ connect(&m_service, &CredentialsService::credentialsNotFound,
2361+ this, &Network::credentialsNotFound);
2362+ connect(&m_service, &SSOService::credentialsStored,
2363+ this, &Network::handleCredentialsStored);
2364+ connect(&m_service, &CredentialsService::loginError,
2365+ this, &Network::loginError);
2366+ connect(&m_service, &SSOService::twoFactorAuthRequired,
2367+ this, &Network::twoFactorAuthRequired);
2368+}
2369+
2370+void Network::getCredentials()
2371+{
2372+ qDebug() << "getting credentials";
2373+ m_service.getCredentials();
2374+}
2375+
2376+void Network::setCredentials(Token token)
2377+{
2378+ m_service.setCredentials(token);
2379+}
2380+
2381+QMap<QString, QString> buildCurrencyMap()
2382+{
2383+ /* NOTE: The list of currencies we need to handle mapping symbols of.
2384+ * Please keep this list in A-Z order.
2385+ */
2386+ QMap<QString, QString> currencies_map {
2387+ { "CNY", "RMB"},
2388+ { "EUR", "€"},
2389+ { "GBP", "₤"},
2390+ { "HKD", "HK$"},
2391+ { "TWD", "TW$"},
2392+ { "USD", "US$"},
2393+ };
2394+ return currencies_map;
2395+}
2396+
2397+const QString DEFAULT_CURRENCY {"USD"};
2398+
2399+QString Network::getSymbolForCurrency(const QString &currency_code)
2400+{
2401+ static QMap<QString, QString> currency_map = buildCurrencyMap();
2402+
2403+ if (currency_map.contains(currency_code)) {
2404+ return currency_map[currency_code];
2405+ } else{
2406+ return currency_code;
2407+ }
2408+}
2409+
2410+bool Network::isSupportedCurrency(const QString& currency_code)
2411+{
2412+ static QMap<QString, QString> currency_map = buildCurrencyMap();
2413+
2414+ return currency_map.contains(currency_code);
2415+}
2416+
2417+QString Network::sanitizeUrl(const QUrl& url)
2418+{
2419+ static QRegExp regexp("\\b\\/\\/+");
2420+ return url.toString().replace(regexp, "/");
2421+}
2422+
2423+QString Network::encodeQuerySlashes(const QString& query)
2424+{
2425+ static QRegExp regexp("\\b\\/");
2426+ QString temp(query);
2427+ return temp.replace(regexp, "%2F");
2428+}
2429+
2430+void Network::onReply(QNetworkReply *reply)
2431+{
2432+ QVariant statusAttr = reply->attribute(
2433+ QNetworkRequest::HttpStatusCodeAttribute);
2434+ if (!statusAttr.isValid()) {
2435+ QString message("Invalid reply status");
2436+ qWarning() << message;
2437+ Q_EMIT error(message);
2438+ return;
2439+ }
2440+
2441+ RequestObject* state = qobject_cast<RequestObject*>(reply->request().originatingObject());
2442+ if (state->operation.isEmpty()) {
2443+ QString message("Reply received for non valid state.");
2444+ qWarning() << message;
2445+ Q_EMIT error(message);
2446+ return;
2447+ }
2448+
2449+ int httpStatus = statusAttr.toInt();
2450+ qDebug() << "Reply status:" << httpStatus;
2451+ if (httpStatus == 200 || httpStatus == 201) {
2452+ QByteArray payload = reply->readAll();
2453+ qDebug() << payload;
2454+ QJsonDocument document = QJsonDocument::fromJson(payload);
2455+
2456+ if (state->operation.contains(PAYMENT_TYPES) && document.isArray()) {
2457+ qDebug() << "Reply state: PAYMENT_TYPES";
2458+ QVariantList listPays;
2459+ QJsonArray array = document.array();
2460+ for (int i = 0; i < array.size(); i++) {
2461+ QJsonObject object = array.at(i).toObject();
2462+ QString description = object.value("description").toString();
2463+ QString backend = object.value("id").toString();
2464+ bool preferredBackend = object.value("preferred").toBool();
2465+ QJsonArray choices = object.value("choices").toArray();
2466+ for (int j = 0; j < choices.size(); j++) {
2467+ QJsonObject choice = choices.at(j).toObject();
2468+ QString name = choice.value("description").toString();
2469+ QString paymentId = QString::number(choice.value("id").toInt());
2470+ bool requiresInteracion = choice.value("requires_interaction").toBool();
2471+ bool preferred = choice.value("preferred").toBool() && preferredBackend;
2472+ PayInfo* pay = new PayInfo();
2473+ pay->setPayData(name, description, paymentId, backend, requiresInteracion, preferred);
2474+ listPays.append(qVariantFromValue((QObject*)pay));
2475+ if (preferred) {
2476+ m_preferred = pay;
2477+ }
2478+ }
2479+ }
2480+ qDebug() << "Emit signal paymentTypesObtained";
2481+ Q_EMIT paymentTypesObtained(listPays);
2482+ if (m_preferred == nullptr) {
2483+ Q_EMIT noPreferredPaymentMethod();
2484+ }
2485+ qDebug() << "Emit signal certificateFound";
2486+ CertificateAdapter* cert = new CertificateAdapter(reply->sslConfiguration().peerCertificate());
2487+ Q_EMIT certificateFound(cert);
2488+ } else if (state->operation.contains(BUY_ITEM) && document.isObject()) {
2489+ qDebug() << "Reply state: BUY_ITEM";
2490+ QJsonObject object = document.object();
2491+ QString state = object.value("state").toString();
2492+
2493+ if (state == BUY_COMPLETE) {
2494+ qDebug() << "BUY STATE: complete";
2495+ Q_EMIT buyItemSucceeded();
2496+ } else if (state == BUY_IN_PROGRESS) {
2497+ QUrl url(getPayApiUrl(object.value("redirect_to").toString()));
2498+ qDebug() << "BUY STATE: in progress";
2499+ qDebug() << "BUY Redirect URL:" << url.toString();
2500+ QString sign = m_token.signUrl(url.toString(), "GET", true);
2501+ url.setQuery(encodeQuerySlashes(sign));
2502+ Q_EMIT buyInteractionRequired(url.toString());
2503+ } else {
2504+ qDebug() << "BUY STATE: failed";
2505+ Q_EMIT buyItemFailed();
2506+ }
2507+ } else if (state->operation.contains(ITEM_INFO) && document.isObject()) {
2508+ qDebug() << "Reply state: ITEM_INFO";
2509+ QJsonObject object = document.object();
2510+ QString icon;
2511+ QString publisher;
2512+ if (object.contains("publisher")) {
2513+ publisher = object.value("publisher").toString();
2514+ }
2515+ if (object.contains("icon_url")) {
2516+ icon = object.value("icon_url").toString();
2517+ } else if (object.contains("icon")) {
2518+ icon = object.value("icon").toString();
2519+ }
2520+
2521+ QString title = object.value("title").toString();
2522+
2523+ QJsonObject prices = object.value("prices").toObject();
2524+ QString suggested_currency = DEFAULT_CURRENCY;
2525+ QString currency = DEFAULT_CURRENCY;
2526+ if (reply->hasRawHeader(SUGGESTED_CURRENCY_HEADER_NAME)) {
2527+ suggested_currency = reply->rawHeader(SUGGESTED_CURRENCY_HEADER_NAME);
2528+ }
2529+ const char* env_value = std::getenv(CURRENCY_ENVVAR);
2530+ if (env_value != NULL) {
2531+ suggested_currency = env_value;
2532+ }
2533+
2534+ if (isSupportedCurrency(suggested_currency) && prices.contains(suggested_currency)) {
2535+ currency = suggested_currency;
2536+ }
2537+ double price = 0.00;
2538+ if (prices[currency].isDouble()) {
2539+ price = prices[currency].toDouble();
2540+ } else if (prices[currency].isString()) {
2541+ price = prices[currency].toString().toDouble();
2542+ }
2543+ QLocale locale;
2544+ QString formatted_price = locale.toCurrencyString(price, getSymbolForCurrency(currency));
2545+ qDebug() << "Sending signal: itemDetailsObtained: " << title << " " << formatted_price;
2546+ Q_EMIT itemDetailsObtained(title, publisher, currency, formatted_price, icon);
2547+ } else if (state->operation.contains(CHECK_PURCHASED)) {
2548+ QJsonObject object = document.object();
2549+ auto state = object.value("state").toString();
2550+ if (state == "Complete" || state == "purchased "||
2551+ state == "approved") {
2552+ Q_EMIT buyItemSucceeded();
2553+ } else {
2554+ Q_EMIT itemNotPurchased();
2555+ }
2556+ } else {
2557+ QString message("Reply received for non valid state.");
2558+ qWarning() << message;
2559+ Q_EMIT error(message);
2560+ }
2561+
2562+ } else if (httpStatus == 401 || httpStatus == 403) {
2563+ qWarning() << "Credentials no longer valid. Invalidating.";
2564+ m_service.invalidateCredentials();
2565+ Q_EMIT authenticationError();
2566+ } else if (httpStatus == 404 && state->operation.contains(CHECK_PURCHASED)) {
2567+ Q_EMIT itemNotPurchased();
2568+ } else {
2569+ QString message(QString::number(httpStatus));
2570+ message += ": ";
2571+ message += reply->readAll().data();
2572+ qWarning() << message;
2573+ Q_EMIT error(message);
2574+ }
2575+ reply->deleteLater();
2576+}
2577+
2578+void Network::requestPaymentTypes(const QString& currency)
2579+{
2580+ QNetworkRequest request;
2581+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
2582+ QUrl url(getPayApiUrl(QString(PAY_API_ROOT) + PAY_PAYMENTMETHODS_PATH + "/"));
2583+ QUrlQuery query;
2584+ query.addQueryItem("currency", currency);
2585+ url.setQuery(query);
2586+ qDebug() << "Request Payment Types:" << url.toString();
2587+ signRequestUrl(request, url.toString());
2588+ request.setRawHeader("Accept", "application/json");
2589+ request.setUrl(url);
2590+ RequestObject* reqObject = new RequestObject(QString(PAYMENT_TYPES));
2591+ request.setOriginatingObject(reqObject);
2592+ m_nam.get(request);
2593+}
2594+
2595+void Network::checkPassword(const QString& email, const QString& password,
2596+ const QString& otp, bool purchasing)
2597+{
2598+ m_startPurchase = purchasing;
2599+ m_service.login(email, password, otp);
2600+}
2601+
2602+void Network::buyItemWithPreferredPaymentType(const QString& email, const QString& password, const QString& otp,
2603+ const QString& appid, const QString& itemid, const QString& currency,
2604+ bool recentLogin)
2605+{
2606+ m_selectedPaymentId = m_preferred->paymentId();
2607+ m_selectedBackendId = m_preferred->backendId();
2608+ m_selectedAppId = appid;
2609+ m_selectedItemId = itemid;
2610+ m_currency = currency;
2611+ if (recentLogin) {
2612+ purchaseProcess();
2613+ } else {
2614+ checkPassword(email, password, otp, true);
2615+ }
2616+}
2617+
2618+void Network::buyItem(const QString& email, const QString& password,
2619+ const QString& otp,
2620+ const QString& appid, const QString& itemid, const QString& currency,
2621+ const QString& paymentId, const QString& backendId, bool recentLogin)
2622+{
2623+ m_selectedPaymentId = paymentId;
2624+ m_selectedBackendId = backendId;
2625+ m_selectedAppId = appid;
2626+ m_selectedItemId = itemid;
2627+ m_currency = currency;
2628+ if (recentLogin) {
2629+ purchaseProcess();
2630+ } else {
2631+ checkPassword(email, password, otp, true);
2632+ }
2633+}
2634+
2635+void Network::purchaseProcess()
2636+{
2637+ QUrl url(getPayApiUrl(QString(PAY_API_ROOT) + PAY_PURCHASES_PATH + "/"));
2638+ qDebug() << "Request Purchase:" << url;
2639+ qDebug() << "Payment" << m_selectedAppId << m_selectedBackendId << m_selectedPaymentId;
2640+ QJsonObject serializer;
2641+
2642+ serializer.insert("name", m_selectedAppId);
2643+ if (!m_selectedItemId.isEmpty()) {
2644+ serializer.insert("item_sku", m_selectedItemId);
2645+ }
2646+ serializer.insert("backend_id", m_selectedBackendId);
2647+ serializer.insert("method_id", m_selectedPaymentId);
2648+ serializer.insert("currency", m_currency);
2649+ QJsonDocument doc(serializer);
2650+
2651+ QByteArray content = doc.toJson();
2652+
2653+ QNetworkRequest request;
2654+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
2655+ request.setRawHeader(DEVICE_ID_HEADER, getDeviceId().toUtf8().data());
2656+
2657+ // Get the partner ID and add it to the request.
2658+ QByteArray partner_id = getPartnerId();
2659+ if (!partner_id.isEmpty()) {
2660+ request.setRawHeader(PARTNER_ID_HEADER, partner_id);
2661+ }
2662+ request.setUrl(url);
2663+ signRequestUrl(request, url.toString(), QString("POST"));
2664+ RequestObject* reqObject = new RequestObject(QString(BUY_ITEM));
2665+ request.setOriginatingObject(reqObject);
2666+ m_nam.post(request, content);
2667+}
2668+
2669+QByteArray Network::getPartnerId()
2670+{
2671+ QByteArray id;
2672+
2673+ if (QFile::exists(PARTNER_ID_FILE)) {
2674+ QFile pid_file(PARTNER_ID_FILE);
2675+ if (pid_file.open(QIODevice::ReadOnly | QIODevice::Text)) {
2676+ // Always use lowercase, and trim whitespace.
2677+ id = pid_file.readLine().toLower().trimmed();
2678+ qDebug() << "Found partner ID:" << id;
2679+ } else {
2680+ qWarning() << "Failed to open partner ID file.";
2681+ }
2682+ } else {
2683+ qDebug() << "No partner ID file found.";
2684+ }
2685+
2686+ return id;
2687+}
2688+
2689+QString Network::getDeviceId()
2690+{
2691+ QDBusInterface iface("com.ubuntu.WhoopsiePreferences",
2692+ "/com/ubuntu/WhoopsiePreferences",
2693+ "com.ubuntu.WhoopsiePreferences",
2694+ QDBusConnection::systemBus(), 0);
2695+ QDBusReply<QString> reply = iface.call("GetIdentifier");
2696+ return reply.value();
2697+}
2698+
2699+void Network::getItemInfo(const QString& packagename, const QString& sku)
2700+{
2701+ QUrl url;
2702+
2703+ if (sku.isEmpty()) {
2704+ url = getSearchApiUrl(QString(SEARCH_API_ROOT) + "/package/" + packagename);
2705+ qDebug() << "Request Item Info:" << url;
2706+ QUrlQuery query;
2707+ query.addQueryItem("fields", "title,description,price,icon_url");
2708+ url.setQuery(query);
2709+ } else {
2710+ url = getPayApiUrl(QString(IAP_API_ROOT) + "/packages/" + packagename + "/items/by-sku/" + sku);
2711+ }
2712+ qDebug() << "Request Item Info:" << url;
2713+
2714+ QNetworkRequest request;
2715+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
2716+ request.setUrl(url);
2717+ signRequestUrl(request, url.toString(), QStringLiteral("GET"));
2718+ RequestObject* reqObject = new RequestObject(QString(ITEM_INFO));
2719+ request.setOriginatingObject(reqObject);
2720+ m_nam.get(request);
2721+}
2722+
2723+QString Network::getEnvironmentValue(const QString& key,
2724+ const QString& defaultValue)
2725+{
2726+ QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
2727+ QString result = environment.value(key, defaultValue);
2728+ return result;
2729+}
2730+
2731+QString Network::getPayApiUrl(const QString& path)
2732+{
2733+ QUrl url(getEnvironmentValue(PAY_BASE_URL_ENVVAR, PAY_BASE_URL).append(path));
2734+ return sanitizeUrl(url);
2735+}
2736+
2737+QString Network::getSearchApiUrl(const QString& path)
2738+{
2739+ QUrl url(getEnvironmentValue(SEARCH_BASE_URL_ENVVAR, SEARCH_BASE_URL).append(path));
2740+ return sanitizeUrl(url);
2741+}
2742+
2743+void Network::signRequestUrl(QNetworkRequest& request, QString url, QString method)
2744+{
2745+ QString sign = m_token.signUrl(url, method);
2746+ request.setRawHeader("Authorization", sign.toUtf8());
2747+}
2748+
2749+QString Network::getAddPaymentUrl(const QString& currency)
2750+{
2751+ QUrl payUrl(getPayApiUrl(QString(PAY_API_ROOT) + PAY_PAYMENTMETHODS_ADD_PATH + "/"));
2752+ QUrlQuery query;
2753+ query.addQueryItem("currency", currency);
2754+ payUrl.setQuery(query);
2755+ qDebug() << "Get Add Payment URL:" << payUrl;
2756+ QString sign = m_token.signUrl(payUrl.toString(),
2757+ QStringLiteral("GET"), true);
2758+ payUrl.setQuery(encodeQuerySlashes(sign));
2759+ return payUrl.toString();
2760+}
2761+
2762+QDateTime Network::getTokenUpdated()
2763+{
2764+ return m_token.updated();
2765+}
2766+
2767+void Network::checkItemPurchased(const QString& appid, const QString& sku)
2768+{
2769+ QUrl url;
2770+ if (sku.isEmpty()) {
2771+ url = getPayApiUrl(QString(PAY_API_ROOT) + PAY_PURCHASES_PATH + "/" + appid + "/");
2772+ } else {
2773+ url = getPayApiUrl(QString(IAP_API_ROOT) + "/packages/" + appid + "/items/by-sku/" + sku);
2774+ }
2775+
2776+ qDebug() << "Checking for previous purchase:" << url;
2777+ QNetworkRequest request;
2778+ request.setUrl(url);
2779+ signRequestUrl(request, url.toString(), QStringLiteral("GET"));
2780+ RequestObject* reqObject = new RequestObject(QString(CHECK_PURCHASED));
2781+ request.setOriginatingObject(reqObject);
2782+ m_nam.get(request);
2783+}
2784+
2785+void Network::handleCredentialsFound(Token token)
2786+{
2787+ m_token = token;
2788+ Q_EMIT credentialsFound();
2789+}
2790+
2791+void Network::handleCredentialsStored()
2792+{
2793+ // Get credentials again to update token object.
2794+ getCredentials();
2795+ if (m_startPurchase) {
2796+ purchaseProcess();
2797+ } else {
2798+ Q_EMIT passwordValid();
2799+ }
2800+}
2801+
2802+}
2803
2804=== added file 'pay-ui/backend/modules/payui/network.h'
2805--- pay-ui/backend/modules/payui/network.h 1970-01-01 00:00:00 +0000
2806+++ pay-ui/backend/modules/payui/network.h 2016-03-11 14:49:24 +0000
2807@@ -0,0 +1,136 @@
2808+/*
2809+ * Copyright 2014-2015 Canonical Ltd.
2810+ *
2811+ * This library is free software; you can redistribute it and/or
2812+ * modify it under the terms of version 3 of the GNU Lesser General Public
2813+ * License as published by the Free Software Foundation.
2814+ *
2815+ * This program is distributed in the hope that it will be useful,
2816+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2817+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
2818+ * General Public License for more details.
2819+ *
2820+ * You should have received a copy of the GNU Lesser General Public
2821+ * License along with this library; if not, write to the
2822+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
2823+ * Boston, MA 02110-1301, USA.
2824+ */
2825+
2826+#ifndef NETWORK_H
2827+#define NETWORK_H
2828+
2829+#include <QDateTime>
2830+#include <QObject>
2831+#include <QtNetwork/QNetworkReply>
2832+#include <QStringList>
2833+#include <QVariantList>
2834+#include <QUrl>
2835+#include <QtNetwork/QNetworkAccessManager>
2836+#include <token.h>
2837+#include "credentials_service.h"
2838+
2839+#include "certificateadapter.h"
2840+#include "pay_info.h"
2841+
2842+using namespace UbuntuOne;
2843+
2844+namespace UbuntuPurchase {
2845+
2846+constexpr static const char* PAY_BASE_URL_ENVVAR{"PAY_BASE_URL"};
2847+constexpr static const char* PAY_BASE_URL{"https://myapps.developer.ubuntu.com"};
2848+constexpr static const char* PAY_API_ROOT{"/api/2.0/click"};
2849+constexpr static const char* IAP_API_ROOT{"/inventory/api/v1"};
2850+constexpr static const char* CURRENCY_ENVVAR {"U1_SEARCH_CURRENCY"};
2851+constexpr static const char* SEARCH_BASE_URL_ENVVAR{"U1_SEARCH_BASE_URL"};
2852+constexpr static const char* SEARCH_BASE_URL{"https://search.apps.ubuntu.com"};
2853+constexpr static const char* SEARCH_API_ROOT{"/api/v1"};
2854+
2855+class RequestObject : public QObject
2856+{
2857+ Q_OBJECT
2858+public:
2859+ explicit RequestObject(QString oper, QObject *parent = 0) :
2860+ QObject(parent)
2861+ {
2862+ operation = oper;
2863+ }
2864+
2865+ QString operation;
2866+};
2867+
2868+class Network : public QObject
2869+{
2870+ Q_OBJECT
2871+public:
2872+ explicit Network(QObject *parent = 0);
2873+
2874+ void requestPaymentTypes(const QString& currency);
2875+ void buyItem(const QString& email, const QString& password,
2876+ const QString& otp, const QString& appid,
2877+ const QString& itemid, const QString& currency, const QString& paymentId,
2878+ const QString& backendId, bool recentLogin);
2879+ void buyItemWithPreferredPaymentType(const QString& email, const QString& password,
2880+ const QString& otp,
2881+ const QString& appid, const QString& itemid, const QString& currency,
2882+ bool recentLogin);
2883+ void getItemInfo(const QString& packagename, const QString& sku);
2884+ void checkPassword(const QString& email, const QString& password,
2885+ const QString& otp, bool purchasing=false);
2886+ void getCredentials();
2887+ void setCredentials(Token token);
2888+ QString getAddPaymentUrl(const QString& currency);
2889+ QDateTime getTokenUpdated();
2890+ void checkItemPurchased(const QString& appid, const QString& sku);
2891+ static QString getSymbolForCurrency(const QString& currency_code);
2892+ static bool isSupportedCurrency(const QString& currency_code);
2893+ static QString sanitizeUrl(const QUrl& url);
2894+ static QString encodeQuerySlashes(const QString& query);
2895+ virtual QString getEnvironmentValue(const QString& key,
2896+ const QString& defaultValue);
2897+ virtual QString getPayApiUrl(const QString& path);
2898+ virtual QString getSearchApiUrl(const QString& path);
2899+
2900+Q_SIGNALS:
2901+ void itemDetailsObtained(QString title, QString publisher, QString currency, QString formatted_price, QString icon);
2902+ void paymentTypesObtained(QVariantList payments);
2903+ void buyItemSucceeded();
2904+ void buyItemFailed();
2905+ void buyInteractionRequired(QString url);
2906+ void error(QString message);
2907+ void authenticationError();
2908+ void credentialsNotFound();
2909+ void credentialsFound();
2910+ void passwordValid();
2911+ void noPreferredPaymentMethod();
2912+ void loginError(const QString& message);
2913+ void twoFactorAuthRequired();
2914+ void itemNotPurchased();
2915+ void certificateFound(QObject* cert);
2916+
2917+private Q_SLOTS:
2918+ void onReply(QNetworkReply*);
2919+ void handleCredentialsFound(Token token);
2920+ void handleCredentialsStored();
2921+ void purchaseProcess();
2922+
2923+private:
2924+ QNetworkAccessManager m_nam;
2925+ QNetworkRequest m_request;
2926+ CredentialsService m_service;
2927+ Token m_token;
2928+ PayInfo* m_preferred;
2929+ QString m_selectedPaymentId;
2930+ QString m_selectedBackendId;
2931+ QString m_selectedAppId;
2932+ QString m_selectedItemId;
2933+ QString m_currency;
2934+ bool m_startPurchase = false;
2935+
2936+ void signRequestUrl(QNetworkRequest& request, QString url, QString method="GET");
2937+ QString getDeviceId();
2938+ QByteArray getPartnerId();
2939+};
2940+
2941+}
2942+
2943+#endif // NETWORK_H
2944
2945=== added file 'pay-ui/backend/modules/payui/oxideconstants.cpp'
2946--- pay-ui/backend/modules/payui/oxideconstants.cpp 1970-01-01 00:00:00 +0000
2947+++ pay-ui/backend/modules/payui/oxideconstants.cpp 2016-03-11 14:49:24 +0000
2948@@ -0,0 +1,19 @@
2949+/*
2950+ * Copyright 2014 Canonical Ltd.
2951+ *
2952+ * This program is free software; you can redistribute it and/or
2953+ * modify it under the terms of version 3 of the GNU Lesser General Public
2954+ * License as published by the Free Software Foundation.
2955+ *
2956+ * This program is distributed in the hope that it will be useful,
2957+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2958+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
2959+ * General Public License for more details.
2960+ *
2961+ * You should have received a copy of the GNU Lesser General Public
2962+ * License along with this library; if not, write to the
2963+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
2964+ * Boston, MA 02110-1301, USA.
2965+ */
2966+
2967+#include "oxideconstants.h"
2968
2969=== added file 'pay-ui/backend/modules/payui/oxideconstants.h'
2970--- pay-ui/backend/modules/payui/oxideconstants.h 1970-01-01 00:00:00 +0000
2971+++ pay-ui/backend/modules/payui/oxideconstants.h 2016-03-11 14:49:24 +0000
2972@@ -0,0 +1,53 @@
2973+/*
2974+ * Copyright 2014 Canonical Ltd.
2975+ *
2976+ * This program is free software; you can redistribute it and/or
2977+ * modify it under the terms of version 3 of the GNU Lesser General Public
2978+ * License as published by the Free Software Foundation.
2979+ *
2980+ * This program is distributed in the hope that it will be useful,
2981+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2982+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
2983+ * General Public License for more details.
2984+ *
2985+ * You should have received a copy of the GNU Lesser General Public
2986+ * License along with this library; if not, write to the
2987+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
2988+ * Boston, MA 02110-1301, USA.
2989+ */
2990+
2991+#ifndef OXIDECONSTANTS_H
2992+#define OXIDECONSTANTS_H
2993+
2994+#include <QObject>
2995+
2996+class SecurityStatus : public QObject
2997+{
2998+ Q_OBJECT
2999+ Q_ENUMS(SecurityLevel)
3000+public:
3001+ enum SecurityLevel {
3002+ SecurityLevelNone,
3003+ SecurityLevelSecure,
3004+ SecurityLevelSecureEV,
3005+ SecurityLevelWarning,
3006+ SecurityLevelError
3007+ };
3008+};
3009+
3010+class SslCertificate : public QObject
3011+{
3012+ Q_OBJECT
3013+ Q_ENUMS(PrincipalAttr)
3014+public:
3015+ enum PrincipalAttr {
3016+ PrincipalAttrOrganizationName,
3017+ PrincipalAttrCommonName,
3018+ PrincipalAttrLocalityName,
3019+ PrincipalAttrOrganizationUnitName,
3020+ PrincipalAttrCountryName,
3021+ PrincipalAttrStateOrProvinceName
3022+ };
3023+};
3024+
3025+#endif // OXIDECONSTANTS_H
3026
3027=== added file 'pay-ui/backend/modules/payui/pay_info.cpp'
3028--- pay-ui/backend/modules/payui/pay_info.cpp 1970-01-01 00:00:00 +0000
3029+++ pay-ui/backend/modules/payui/pay_info.cpp 2016-03-11 14:49:24 +0000
3030@@ -0,0 +1,37 @@
3031+/*
3032+ * Copyright 2014 Canonical Ltd.
3033+ *
3034+ * This library is free software; you can redistribute it and/or
3035+ * modify it under the terms of version 3 of the GNU Lesser General Public
3036+ * License as published by the Free Software Foundation.
3037+ *
3038+ * This program is distributed in the hope that it will be useful,
3039+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3040+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
3041+ * General Public License for more details.
3042+ *
3043+ * You should have received a copy of the GNU Lesser General Public
3044+ * License along with this library; if not, write to the
3045+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
3046+ * Boston, MA 02110-1301, USA.
3047+ */
3048+
3049+#include "pay_info.h"
3050+namespace UbuntuPurchase {
3051+
3052+PayInfo::PayInfo(QObject *parent) :
3053+ QObject(parent)
3054+{
3055+}
3056+
3057+void PayInfo::setPayData(QString name, QString description, QString payment, QString backend, bool requiresInteracion, bool preferred)
3058+{
3059+ m_name = name;
3060+ m_paymentId = payment;
3061+ m_description = description;
3062+ m_backendId = backend;
3063+ m_requiresInteracion = requiresInteracion;
3064+ m_preferred = preferred;
3065+}
3066+
3067+}
3068
3069=== added file 'pay-ui/backend/modules/payui/pay_info.h'
3070--- pay-ui/backend/modules/payui/pay_info.h 1970-01-01 00:00:00 +0000
3071+++ pay-ui/backend/modules/payui/pay_info.h 2016-03-11 14:49:24 +0000
3072@@ -0,0 +1,73 @@
3073+/*
3074+ * Copyright 2014 Canonical Ltd.
3075+ *
3076+ * This library is free software; you can redistribute it and/or
3077+ * modify it under the terms of version 3 of the GNU Lesser General Public
3078+ * License as published by the Free Software Foundation.
3079+ *
3080+ * This program is distributed in the hope that it will be useful,
3081+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3082+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
3083+ * General Public License for more details.
3084+ *
3085+ * You should have received a copy of the GNU Lesser General Public
3086+ * License along with this library; if not, write to the
3087+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
3088+ * Boston, MA 02110-1301, USA.
3089+ */
3090+
3091+#ifndef PAY_INFO_H
3092+#define PAY_INFO_H
3093+
3094+#include <QObject>
3095+#include <QString>
3096+
3097+namespace UbuntuPurchase {
3098+
3099+class PayInfo : public QObject
3100+{
3101+ Q_OBJECT
3102+ Q_PROPERTY(QString name READ name NOTIFY nameChanged)
3103+ Q_PROPERTY(QString paymentId READ paymentId NOTIFY paymentIdChanged)
3104+ Q_PROPERTY(QString backendId READ backendId NOTIFY backendIdChanged)
3105+ Q_PROPERTY(QString description READ description NOTIFY descriptionChanged)
3106+ Q_PROPERTY(bool requiresInteracion READ requiresInteracion NOTIFY requiresInteracionChanged)
3107+ Q_PROPERTY(bool preferred READ preferred WRITE setPreferred NOTIFY preferredChanged)
3108+
3109+public:
3110+ explicit PayInfo(QObject *parent = 0);
3111+
3112+ QString name() { return m_name; }
3113+ QString paymentId() { return m_paymentId; }
3114+ QString backendId() { return m_backendId; }
3115+ QString description() { return m_description; }
3116+ bool requiresInteracion() { return m_requiresInteracion; }
3117+ bool preferred() { return m_preferred; }
3118+
3119+ void setPayData(QString name, QString description, QString payment, QString backend, bool steps, bool preferred);
3120+ void setName(const QString& name) { m_name = name; Q_EMIT nameChanged(); }
3121+ void setDescription(const QString& description) { m_description = description; Q_EMIT descriptionChanged(); }
3122+ void setPaymentId(const QString& paymentId) { m_paymentId = paymentId; Q_EMIT paymentIdChanged(); }
3123+ void setbackendId(const QString& backendId) { m_backendId = backendId; Q_EMIT backendIdChanged(); }
3124+ void setRequiresInteracion(bool requiresInteracion) { m_requiresInteracion = requiresInteracion; Q_EMIT requiresInteracionChanged(); }
3125+ void setPreferred(bool preferred) { m_preferred = preferred; Q_EMIT preferredChanged(); }
3126+
3127+Q_SIGNALS:
3128+ void nameChanged();
3129+ void paymentIdChanged();
3130+ void backendIdChanged();
3131+ void descriptionChanged();
3132+ void requiresInteracionChanged();
3133+ void preferredChanged();
3134+
3135+private:
3136+ QString m_name;
3137+ QString m_paymentId;
3138+ QString m_backendId;
3139+ QString m_description;
3140+ bool m_requiresInteracion;
3141+ bool m_preferred;
3142+};
3143+}
3144+
3145+#endif // PAY_INFO_H
3146
3147=== added file 'pay-ui/backend/modules/payui/purchase.cpp'
3148--- pay-ui/backend/modules/payui/purchase.cpp 1970-01-01 00:00:00 +0000
3149+++ pay-ui/backend/modules/payui/purchase.cpp 2016-03-11 14:49:24 +0000
3150@@ -0,0 +1,154 @@
3151+/*
3152+ * Copyright 2014-2016 Canonical Ltd.
3153+ *
3154+ * This library is free software; you can redistribute it and/or
3155+ * modify it under the terms of version 3 of the GNU Lesser General Public
3156+ * License as published by the Free Software Foundation.
3157+ *
3158+ * This program is distributed in the hope that it will be useful,
3159+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3160+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
3161+ * General Public License for more details.
3162+ *
3163+ * You should have received a copy of the GNU Lesser General Public
3164+ * License along with this library; if not, write to the
3165+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
3166+ * Boston, MA 02110-1301, USA.
3167+ */
3168+
3169+#include "purchase.h"
3170+#include <QUrl>
3171+#include <QUrlQuery>
3172+#include <QCoreApplication>
3173+#include <QDateTime>
3174+#include <QtDebug>
3175+
3176+#include <logging.h>
3177+
3178+namespace UbuntuPurchase {
3179+
3180+Purchase::Purchase(QObject *parent) :
3181+ QObject(parent),
3182+ m_network(this)
3183+{
3184+ connect(&m_network, &Network::itemDetailsObtained,
3185+ this, &Purchase::itemDetailsObtained);
3186+ connect(&m_network, &Network::paymentTypesObtained,
3187+ this, &Purchase::paymentTypesObtained);
3188+ connect(&m_network, &Network::buyItemSucceeded,
3189+ this, &Purchase::buyItemSucceeded);
3190+ connect(&m_network, &Network::buyItemFailed,
3191+ this, &Purchase::buyItemFailed);
3192+ connect(&m_network, &Network::buyInteractionRequired,
3193+ this, &Purchase::buyInterationRequired);
3194+ connect(&m_network, &Network::error,
3195+ this, &Purchase::error);
3196+ connect(&m_network, &Network::authenticationError,
3197+ this, &Purchase::authenticationError);
3198+ connect(&m_network, &Network::credentialsNotFound,
3199+ this, &Purchase::credentialsNotFound);
3200+ connect(&m_network, &Network::credentialsFound,
3201+ this, &Purchase::credentialsFound);
3202+ connect(&m_network, &Network::passwordValid,
3203+ this, &Purchase::passwordValid);
3204+ connect(&m_network, &Network::noPreferredPaymentMethod,
3205+ this, &Purchase::noPreferredPaymentMethod);
3206+ connect(&m_network, &Network::loginError,
3207+ this, &Purchase::loginError);
3208+ connect(&m_network, &Network::twoFactorAuthRequired,
3209+ this, &Purchase::twoFactorAuthRequired);
3210+ connect(&m_network, &Network::itemNotPurchased,
3211+ this, &Purchase::itemNotPurchased);
3212+ connect(&m_network, &Network::certificateFound,
3213+ this, &Purchase::certificateFound);
3214+
3215+ QCoreApplication* instance = QCoreApplication::instance();
3216+
3217+ // Set up logging
3218+ UbuntuOne::AuthLogger::setupLogging();
3219+ const char* u1_debug = getenv("U1_DEBUG");
3220+ if (u1_debug != NULL && strcmp(u1_debug, "") != 0) {
3221+ UbuntuOne::AuthLogger::setLogLevel(QtDebugMsg);
3222+ }
3223+
3224+ // Handle the purchase: URI
3225+ for (int i = 0; i < instance->arguments().size(); i++) {
3226+ QString argument = instance->arguments().at(i);
3227+ if (argument.startsWith("purchase://")) {
3228+ QUrl data(argument);
3229+ m_appid = data.host();
3230+ m_itemid = data.fileName();
3231+ qDebug() << "Purchase requested for" << m_itemid << "by app" << m_appid;
3232+ break;
3233+ }
3234+ }
3235+}
3236+
3237+Purchase::~Purchase()
3238+{
3239+ UbuntuOne::AuthLogger::stopLogging();
3240+}
3241+
3242+void Purchase::quitCancel()
3243+{
3244+ qDebug() << "Purchase Canceled: exit code 1";
3245+ QCoreApplication::exit(1);
3246+}
3247+
3248+void Purchase::quitSuccess()
3249+{
3250+ qDebug() << "Purchase Succeeded: exit code 0";
3251+ QCoreApplication::exit(0);
3252+}
3253+
3254+void Purchase::checkCredentials()
3255+{
3256+ m_network.getCredentials();
3257+}
3258+
3259+QString Purchase::getAddPaymentUrl(QString currency)
3260+{
3261+ return m_network.getAddPaymentUrl(currency);
3262+}
3263+
3264+void Purchase::getItemDetails()
3265+{
3266+ if (m_appid.isEmpty() && m_itemid.isEmpty()) {
3267+ qCritical() << "AppId or ItemId not provided";
3268+ quitCancel();
3269+ }
3270+ qDebug() << "Getting Item Details";
3271+ m_network.getItemInfo(m_appid, m_itemid);
3272+}
3273+
3274+void Purchase::getPaymentTypes(const QString& currency)
3275+{
3276+ m_network.requestPaymentTypes(currency);
3277+}
3278+
3279+void Purchase::checkWallet(QString email, QString password, QString otp)
3280+{
3281+ m_network.checkPassword(email, password, otp);
3282+}
3283+
3284+void Purchase::buyItemWithPreferredPayment(QString email, QString password, QString otp, QString currency, bool recentLogin)
3285+{
3286+ m_network.buyItemWithPreferredPaymentType(email, password, otp, m_appid, m_itemid, currency, recentLogin);
3287+}
3288+
3289+void Purchase::buyItem(QString email, QString password, QString otp, QString currency, QString paymentId, QString backendId, bool recentLogin)
3290+{
3291+ m_network.buyItem(email, password, otp, m_appid, m_itemid, currency, paymentId, backendId, recentLogin);
3292+}
3293+
3294+QDateTime Purchase::getTokenUpdated()
3295+{
3296+ return m_network.getTokenUpdated();
3297+}
3298+
3299+void Purchase::checkItemPurchased()
3300+{
3301+ m_network.checkItemPurchased(m_appid, m_itemid);
3302+}
3303+
3304+}
3305
3306=== added file 'pay-ui/backend/modules/payui/purchase.h'
3307--- pay-ui/backend/modules/payui/purchase.h 1970-01-01 00:00:00 +0000
3308+++ pay-ui/backend/modules/payui/purchase.h 2016-03-11 14:49:24 +0000
3309@@ -0,0 +1,59 @@
3310+#ifndef PURCHASE_H
3311+#define PURCHASE_H
3312+
3313+#include <QDateTime>
3314+#include <QObject>
3315+#include <QString>
3316+
3317+#include "network.h"
3318+#include "certificateadapter.h"
3319+
3320+namespace UbuntuPurchase {
3321+
3322+class Purchase : public QObject
3323+{
3324+ Q_OBJECT
3325+
3326+public:
3327+ explicit Purchase(QObject *parent = 0);
3328+ ~Purchase();
3329+
3330+ Q_INVOKABLE void getItemDetails();
3331+ Q_INVOKABLE void getPaymentTypes(const QString &currency);
3332+ Q_INVOKABLE void buyItemWithPreferredPayment(QString email, QString password, QString otp, QString currency, bool recentLogin);
3333+ Q_INVOKABLE void buyItem(QString email, QString password, QString otp, QString currency, QString paymentId, QString backendId, bool recentLogin);
3334+ Q_INVOKABLE void checkCredentials();
3335+ Q_INVOKABLE QString getAddPaymentUrl(QString currency);
3336+ Q_INVOKABLE void checkWallet(QString email, QString password, QString otp);
3337+
3338+ Q_INVOKABLE void quitSuccess();
3339+ Q_INVOKABLE void quitCancel();
3340+ Q_INVOKABLE QDateTime getTokenUpdated();
3341+ Q_INVOKABLE void checkItemPurchased();
3342+
3343+Q_SIGNALS:
3344+ void itemDetailsObtained(QString title, QString publisher, QString currency, QString formatted_price, QString icon);
3345+ void paymentTypesObtained(QVariantList payments);
3346+ void buyItemSucceeded();
3347+ void buyItemFailed();
3348+ void buyInterationRequired(QString url);
3349+ void error(QString message);
3350+ void authenticationError();
3351+ void credentialsNotFound();
3352+ void credentialsFound();
3353+ void passwordValid();
3354+ void noPreferredPaymentMethod();
3355+ void loginError(const QString& message);
3356+ void twoFactorAuthRequired();
3357+ void itemNotPurchased();
3358+ void certificateFound(QObject* cert);
3359+
3360+private:
3361+ Network m_network;
3362+ QString m_appid;
3363+ QString m_itemid;
3364+};
3365+
3366+}
3367+
3368+#endif // PURCHASE_H
3369
3370=== added file 'pay-ui/backend/modules/payui/qmldir'
3371--- pay-ui/backend/modules/payui/qmldir 1970-01-01 00:00:00 +0000
3372+++ pay-ui/backend/modules/payui/qmldir 2016-03-11 14:49:24 +0000
3373@@ -0,0 +1,2 @@
3374+module payui
3375+plugin payuibackend
3376
3377=== added directory 'pay-ui/backend/tests'
3378=== added file 'pay-ui/backend/tests/CMakeLists.txt'
3379--- pay-ui/backend/tests/CMakeLists.txt 1970-01-01 00:00:00 +0000
3380+++ pay-ui/backend/tests/CMakeLists.txt 2016-03-11 14:49:24 +0000
3381@@ -0,0 +1,30 @@
3382+
3383+include_directories (${CMAKE_CURRENT_SOURCE_DIR}/../payui)
3384+set(CMAKE_INCLUDE_CURRENT_DIR ON)
3385+set(CMAKE_AUTOMOC ON)
3386+find_package(Qt5Core REQUIRED)
3387+
3388+set(PURCHASE_TESTS_TARGET test-purchase-ui)
3389+
3390+add_executable(${PURCHASE_TESTS_TARGET}
3391+ test_network.cpp
3392+)
3393+target_link_libraries(${PURCHASE_TESTS_TARGET}
3394+ -Wl,-rpath,${CMAKE_CURRENT_BINARY_DIR}/../payui
3395+ -L${CMAKE_CURRENT_BINARY_DIR}/../payui
3396+ ${PAYUI_BACKEND}
3397+)
3398+qt5_use_modules(${PURCHASE_TESTS_TARGET} Qml Quick Core DBus Xml Network Test)
3399+
3400+add_custom_target(payui-cppunit-tests
3401+ COMMAND env LANGUAGE=en_US.UTF-8 LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 ${XVFB_CMD} ${CMAKE_CURRENT_BINARY_DIR}/${PURCHASE_TESTS_TARGET}
3402+ DEPENDS ${PURCHASE_TESTS_TARGET} mock_click_server.py
3403+)
3404+
3405+add_test(NAME payui-cppunit-tests
3406+ COMMAND make payui-cppunit-tests
3407+)
3408+
3409+add_custom_target(mock_click_server.py
3410+ COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/mock_click_server.py ${CMAKE_CURRENT_BINARY_DIR}/mock_click_server.py
3411+)
3412
3413=== added file 'pay-ui/backend/tests/mock_click_server.py'
3414--- pay-ui/backend/tests/mock_click_server.py 1970-01-01 00:00:00 +0000
3415+++ pay-ui/backend/tests/mock_click_server.py 2016-03-11 14:49:24 +0000
3416@@ -0,0 +1,162 @@
3417+import json
3418+import threading
3419+from http.server import BaseHTTPRequestHandler, HTTPServer
3420+
3421+
3422+KEEP_ALIVE = True
3423+
3424+
3425+class MyHandler(BaseHTTPRequestHandler):
3426+
3427+ def do_HEAD(self):
3428+ self.send_response(200)
3429+ self.send_header("X-Click-Token", "X-Click-Token")
3430+ self.end_headers()
3431+
3432+ def response_payment_types(self, fail=False):
3433+ types = [
3434+ {
3435+ "description": "PayPal",
3436+ "id": "paypal",
3437+ "preferred": False,
3438+ "choices": [
3439+ {
3440+ "currencies": [
3441+ "USD",
3442+ "GBP",
3443+ "EUR"
3444+ ],
3445+ "id": 532,
3446+ "requires_interaction": False,
3447+ "preferred": False,
3448+ "description": "PayPal Preapproved Payment (exp. 2014-04-12)"
3449+ }
3450+ ]
3451+ },
3452+ {
3453+ "description": "Credit or Debit Card",
3454+ "id": "credit_card",
3455+ "preferred": True,
3456+ "choices": [
3457+ {
3458+ "currencies": [
3459+ "USD"
3460+ ],
3461+ "id": 1767,
3462+ "requires_interaction": False,
3463+ "preferred": False,
3464+ "description": "**** **** **** 1111 (Visa, exp. 02/2015)"
3465+ },
3466+ {
3467+ "currencies": [
3468+ "USD"
3469+ ],
3470+ "id": 1726,
3471+ "requires_interaction": False,
3472+ "preferred": True,
3473+ "description": "**** **** **** 1111 (Visa, exp. 03/2015)"
3474+ }
3475+ ]
3476+ }
3477+ ]
3478+ if fail:
3479+ self.send_response(404)
3480+ else:
3481+ self.send_response(200)
3482+ self.send_header("Content-type", "application/json")
3483+ self.end_headers()
3484+ self.wfile.write(bytes(json.dumps(types), 'UTF-8'))
3485+
3486+ def response_buy_item(self, fail=False, interaction=False):
3487+ state = "Complete" if not interaction else "InProgress"
3488+ response = {
3489+ "state": state,
3490+ }
3491+ if interaction:
3492+ # Real server returns path starting with / here.
3493+ response["redirect_to"] = "/redirect.url?currency=USD"
3494+ if fail:
3495+ response = {}
3496+
3497+ if self.path.find("/notpurchased/") != -1:
3498+ self.send_response(404)
3499+ else:
3500+ self.send_response(200)
3501+
3502+ self.send_header("Content-type", "application/json")
3503+ self.end_headers()
3504+ self.wfile.write(bytes(json.dumps(response), 'UTF-8'))
3505+
3506+ def response_item_info(self, fail, eurozone, dotar):
3507+ response = {
3508+ "title": "title",
3509+ "publisher": "publisher",
3510+ "price": 9.99,
3511+ "prices": {
3512+ "USD": 1.99,
3513+ "EUR": 1.69,
3514+ "GBP": 1.29,
3515+ "ARS": 18.05,
3516+ },
3517+ "icon_url": "icon_url",
3518+ }
3519+ if fail:
3520+ self.send_response(404)
3521+ else:
3522+ self.send_response(200)
3523+ self.send_header("Content-type", "application/json")
3524+ if eurozone:
3525+ self.send_header("X-Suggested-Currency", "EUR")
3526+ if dotar:
3527+ self.send_header("X-Suggested-Currency", "ARS")
3528+ self.end_headers()
3529+ self.wfile.write(bytes(json.dumps(response), 'UTF-8'))
3530+
3531+ def response_auth_error(self):
3532+ self.send_response(401)
3533+ self.send_header("Content-type", "application/json")
3534+ self.end_headers()
3535+ self.wfile.write(bytes(json.dumps(dict()), 'UTF-8'))
3536+
3537+ def do_POST(self):
3538+ """Respond to a POST request."""
3539+ #content = self.rfile.read(int(self.headers.get('content-length')))
3540+ #structure = json.loads(content.decode('utf-8'))
3541+ self.do_GET()
3542+
3543+ def do_GET(self):
3544+ """Respond to a GET request."""
3545+ if self.path.find("/authError/") != -1:
3546+ self.response_auth_error()
3547+ elif self.path.find("shutdown") != -1:
3548+ global KEEP_ALIVE
3549+ KEEP_ALIVE = False
3550+ elif self.path.find("paymentmethods/") != -1:
3551+ fail = self.path.find("/fail/") != -1
3552+ self.response_payment_types(fail)
3553+ elif self.path.find("purchases/") != -1:
3554+ fail = self.path.find("/fail/") != -1
3555+ interaction = self.path.find("/interaction/") != -1
3556+ self.response_buy_item(fail=fail, interaction=interaction)
3557+ elif self.path.find("iteminfo/") != -1:
3558+ fail = self.path.find("/fail/") != -1
3559+ eurozone = self.path.find("/eurozone/") != -1
3560+ dotar = self.path.find("/dotar/") != -1
3561+ self.response_item_info(fail, eurozone, dotar)
3562+
3563+
3564+def run_click_server():
3565+ server_address = ('', 8000)
3566+ httpd = HTTPServer(server_address, MyHandler)
3567+ global KEEP_ALIVE
3568+ print('start')
3569+ while KEEP_ALIVE:
3570+ httpd.handle_request()
3571+
3572+
3573+def run_mocked_settings():
3574+ t = threading.Thread(target=run_click_server)
3575+ t.start()
3576+
3577+
3578+run_mocked_settings()
3579
3580=== added file 'pay-ui/backend/tests/test_network.cpp'
3581--- pay-ui/backend/tests/test_network.cpp 1970-01-01 00:00:00 +0000
3582+++ pay-ui/backend/tests/test_network.cpp 2016-03-11 14:49:24 +0000
3583@@ -0,0 +1,317 @@
3584+/*
3585+ * Copyright 2014-2016 Canonical Ltd.
3586+ *
3587+ * This library is free software; you can redistribute it and/or
3588+ * modify it under the terms of version 3 of the GNU Lesser General Public
3589+ * License as published by the Free Software Foundation.
3590+ *
3591+ * This program is distributed in the hope that it will be useful,
3592+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3593+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
3594+ * General Public License for more details.
3595+ *
3596+ * You should have received a copy of the GNU Lesser General Public
3597+ * License along with this library; if not, write to the
3598+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
3599+ * Boston, MA 02110-1301, USA.
3600+ */
3601+
3602+#include <stdlib.h>
3603+
3604+#include <QObject>
3605+#include <QProcess>
3606+#include <QProcessEnvironment>
3607+#include <QDebug>
3608+#include <QVariant>
3609+#include <QUrlQuery>
3610+#include <QString>
3611+#include <QDir>
3612+#include <QTest>
3613+#include <QTimer>
3614+#include <QSignalSpy>
3615+#include <QVariant>
3616+#include <QVariantList>
3617+
3618+#include <modules/payui/network.h>
3619+#include <modules/payui/pay_info.h>
3620+
3621+using namespace UbuntuPurchase;
3622+
3623+class TestNetwork: public QObject
3624+{
3625+ Q_OBJECT
3626+
3627+private slots:
3628+ void initTestCase();
3629+ void testNetworkAuthenticationError();
3630+ void testNetworkGetPaymentTypes();
3631+ void testNetworkGetPaymentTypesFail();
3632+ void testNetworkBuyItem();
3633+ void testNetworkBuyItemInProgress();
3634+ void testNetworkBuyItemFail();
3635+ void testNetworkButItemWithPaymentType();
3636+ void testNetworkButItemWithPaymentTypeFail();
3637+ void testNetworkButItemWithPaymentTypeInProgress();
3638+ void testNetworkGetItemInfo();
3639+ void testNetworkGetItemInfoEurozone();
3640+ void testNetworkGetItemInfoOtherCoins();
3641+ void testNetworkGetItemInfoOverride();
3642+ void testNetworkGetItemInfoOverrideOther();
3643+ void testNetworkGetItemInfoFail();
3644+ void testUseExistingCredentials();
3645+ void testCheckAlreadyPurchased();
3646+ void testCheckAlreadyPurchasedFail();
3647+ void testSanitizeUrl();
3648+ void testEncodeQuerySlashes();
3649+ void cleanupTestCase();
3650+
3651+private:
3652+ UbuntuPurchase::Network network;
3653+ QProcess* process;
3654+};
3655+
3656+void TestNetwork::initTestCase()
3657+{
3658+ setenv("SSO_AUTH_BASE_URL", "http://localhost:8000/", 1);
3659+ qDebug() << "Starting Server";
3660+ network.setCredentials(Token("token_key", "token_secret", "consumer_key", "consumer_secret"));
3661+ process = new QProcess(this);
3662+ QSignalSpy spy(process, SIGNAL(started()));
3663+ process->setWorkingDirectory(QDir::currentPath() + "/backend/tests/");
3664+ qDebug() << process->workingDirectory();
3665+ QString program = "python3";
3666+ QString script = "mock_click_server.py";
3667+ process->start(program, QStringList() << script);
3668+ QTRY_COMPARE(spy.count(), 1);
3669+ // Wait for server to start
3670+ QTimer timer;
3671+ QSignalSpy spy2(&timer, SIGNAL(timeout()));
3672+ timer.setInterval(2000);
3673+ timer.setSingleShot(true);
3674+ timer.start();
3675+ QTRY_COMPARE(spy2.count(), 1);
3676+}
3677+
3678+void TestNetwork::testNetworkAuthenticationError()
3679+{
3680+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/authError/", 1);
3681+ QSignalSpy spy(&network, SIGNAL(authenticationError()));
3682+ network.buyItem("email", "password", "otp", "USD", "appid", "itemid", "paymentid", "backendid", false);
3683+ // WTF DOES THIS HAPPEN TWICE
3684+ QTRY_COMPARE(spy.count(), 2);
3685+}
3686+
3687+void TestNetwork::testNetworkGetPaymentTypes()
3688+{
3689+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/", 1);
3690+ QSignalSpy spy(&network, SIGNAL(paymentTypesObtained(QVariantList)));
3691+ network.requestPaymentTypes("USD");
3692+ QTRY_COMPARE(spy.count(), 1);
3693+ QList<QVariant> arguments = spy.takeFirst();
3694+ QVERIFY(arguments.at(0).toList().count() == 3);
3695+ QVariantList listPays = arguments.at(0).toList();
3696+ for (int i = 0; i < listPays.count(); i++) {
3697+ QVariant var = listPays.at(i);
3698+ PayInfo* info = var.value<PayInfo*>();
3699+ if (i == 2) {
3700+ QVERIFY(info->preferred() == true);
3701+ } else {
3702+ QVERIFY(info->preferred() == false);
3703+ }
3704+ }
3705+}
3706+
3707+void TestNetwork::testNetworkGetPaymentTypesFail()
3708+{
3709+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/fail/", 1);
3710+ QSignalSpy spy(&network, SIGNAL(error(QString)));
3711+ network.requestPaymentTypes("USD");
3712+ // WTF DOES THIS HAPPEN TWICE
3713+ QTRY_COMPARE(spy.count(), 2);
3714+ QList<QVariant> arguments = spy.takeFirst();
3715+ QVERIFY(arguments.at(0).toString().startsWith("404:"));
3716+}
3717+
3718+void TestNetwork::testNetworkBuyItem()
3719+{
3720+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/", 1);
3721+ QSignalSpy spy(&network, SIGNAL(buyItemSucceeded()));
3722+ network.buyItem("email", "password", "otp", "USD", "appid", "itemid", "paymentid", "backendid", false);
3723+ QTRY_COMPARE(spy.count(), 1);
3724+}
3725+
3726+void TestNetwork::testNetworkBuyItemInProgress()
3727+{
3728+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/interaction/", 1);
3729+ QSignalSpy spy(&network, SIGNAL(buyInteractionRequired(QString)));
3730+ network.buyItem("email", "password", "otp", "USD", "appid", "itemid", "paymentid", "backendid", false);
3731+ QTRY_COMPARE(spy.count(), 1);
3732+ QList<QVariant> arguments = spy.takeFirst();
3733+ QString url(arguments.at(0).toString());
3734+ QString expected("http://localhost:8000/interaction/redirect.url?currency=USD");
3735+ QVERIFY(url.startsWith(expected));
3736+}
3737+
3738+void TestNetwork::testNetworkBuyItemFail()
3739+{
3740+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/fail/", 1);
3741+ QSignalSpy spy(&network, SIGNAL(buyItemFailed()));
3742+ network.buyItem("email", "password", "otp", "USD", "appid", "itemid", "paymentid", "backendid", false);
3743+ QTRY_COMPARE(spy.count(), 1);
3744+}
3745+
3746+void TestNetwork::testNetworkButItemWithPaymentType()
3747+{
3748+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/", 1);
3749+ QSignalSpy spy(&network, SIGNAL(paymentTypesObtained(QVariantList)));
3750+ network.requestPaymentTypes("USD");
3751+ QTRY_COMPARE(spy.count(), 1);
3752+ QSignalSpy spy2(&network, SIGNAL(buyItemSucceeded()));
3753+ network.buyItemWithPreferredPaymentType("email", "password", "otp", "USD", "appid", "itemid", false);
3754+ QTRY_COMPARE(spy2.count(), 1);
3755+}
3756+
3757+void TestNetwork::testNetworkButItemWithPaymentTypeFail()
3758+{
3759+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/", 1);
3760+ QSignalSpy spy(&network, SIGNAL(paymentTypesObtained(QVariantList)));
3761+ network.requestPaymentTypes("USD");
3762+ QTRY_COMPARE(spy.count(), 1);
3763+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/fail/", 1);
3764+ QSignalSpy spy2(&network, SIGNAL(buyItemFailed()));
3765+ network.buyItemWithPreferredPaymentType("email", "password", "otp", "USD", "appid", "itemid", false);
3766+ QTRY_COMPARE(spy2.count(), 1);
3767+}
3768+
3769+void TestNetwork::testNetworkButItemWithPaymentTypeInProgress()
3770+{
3771+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/", 1);
3772+ QSignalSpy spy(&network, SIGNAL(paymentTypesObtained(QVariantList)));
3773+ network.requestPaymentTypes("USD""USD");
3774+ QTRY_COMPARE(spy.count(), 1);
3775+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/interaction/", 1);
3776+ QSignalSpy spy2(&network, SIGNAL(buyInteractionRequired(QString)));
3777+ network.buyItemWithPreferredPaymentType("email", "password", "otp", "USD", "appid", "itemid", false);
3778+ QTRY_COMPARE(spy2.count(), 1);
3779+}
3780+
3781+void TestNetwork::testNetworkGetItemInfo()
3782+{
3783+ setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/iteminfo/", 1);
3784+ unsetenv(CURRENCY_ENVVAR);
3785+ QSignalSpy spy(&network, SIGNAL(itemDetailsObtained(QString,QString,QString,QString,QString)));
3786+ network.getItemInfo("packagename", "");
3787+ QTRY_COMPARE(spy.count(), 1);
3788+ QList<QVariant> arguments = spy.takeFirst();
3789+ QCOMPARE(arguments.at(2).toString(), QStringLiteral("USD"));
3790+ QCOMPARE(arguments.at(3).toString(), QStringLiteral("US$1.99"));
3791+}
3792+
3793+void TestNetwork::testNetworkGetItemInfoEurozone()
3794+{
3795+ setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/iteminfo/eurozone/", 1);
3796+ unsetenv(CURRENCY_ENVVAR);
3797+ QSignalSpy spy(&network, SIGNAL(itemDetailsObtained(QString,QString,QString,QString,QString)));
3798+ network.getItemInfo("packagename", "");
3799+ QTRY_COMPARE(spy.count(), 1);
3800+ QList<QVariant> arguments = spy.takeFirst();
3801+ QCOMPARE(arguments.at(2).toString(), QStringLiteral("EUR"));
3802+ QCOMPARE(arguments.at(3).toString(), QStringLiteral("€1.69"));
3803+}
3804+
3805+void TestNetwork::testNetworkGetItemInfoOtherCoins()
3806+{
3807+ setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/iteminfo/dotar/", 1);
3808+ unsetenv(CURRENCY_ENVVAR);
3809+ QSignalSpy spy(&network, SIGNAL(itemDetailsObtained(QString,QString,QString,QString,QString)));
3810+ network.getItemInfo("packagename", "");
3811+ QTRY_COMPARE(spy.count(), 1);
3812+ QList<QVariant> arguments = spy.takeFirst();
3813+ QCOMPARE(arguments.at(2).toString(), QStringLiteral("USD"));
3814+ QCOMPARE(arguments.at(3).toString(), QStringLiteral("US$1.99"));
3815+}
3816+
3817+void TestNetwork::testNetworkGetItemInfoOverride()
3818+{
3819+ setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/iteminfo/", 1);
3820+ setenv(CURRENCY_ENVVAR, "EUR", true);
3821+ QSignalSpy spy(&network, SIGNAL(itemDetailsObtained(QString,QString,QString,QString,QString)));
3822+ network.getItemInfo("packagename", "");
3823+ QTRY_COMPARE(spy.count(), 1);
3824+ QList<QVariant> arguments = spy.takeFirst();
3825+ QCOMPARE(arguments.at(2).toString(), QStringLiteral("EUR"));
3826+ QCOMPARE(arguments.at(3).toString(), QStringLiteral("€1.69"));
3827+ unsetenv(CURRENCY_ENVVAR);
3828+}
3829+
3830+void TestNetwork::testNetworkGetItemInfoOverrideOther()
3831+{
3832+ setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/iteminfo/dotar/", 1);
3833+ setenv(CURRENCY_ENVVAR, "EUR", true);
3834+ QSignalSpy spy(&network, SIGNAL(itemDetailsObtained(QString,QString,QString,QString,QString)));
3835+ network.getItemInfo("packagename", "");
3836+ QTRY_COMPARE(spy.count(), 1);
3837+ QList<QVariant> arguments = spy.takeFirst();
3838+ QCOMPARE(arguments.at(2).toString(), QStringLiteral("EUR"));
3839+ QCOMPARE(arguments.at(3).toString(), QStringLiteral("€1.69"));
3840+ unsetenv(CURRENCY_ENVVAR);
3841+}
3842+
3843+void TestNetwork::testNetworkGetItemInfoFail()
3844+{
3845+ setenv(SEARCH_BASE_URL_ENVVAR, "http://localhost:8000/fail/iteminfo/", 1);
3846+ unsetenv(CURRENCY_ENVVAR);
3847+ QSignalSpy spy(&network, SIGNAL(error(QString)));
3848+ network.getItemInfo("packagename", "");
3849+ // WTF DOES THIS HAPPEN TWICE
3850+ QTRY_COMPARE(spy.count(), 2);
3851+}
3852+
3853+void TestNetwork::testUseExistingCredentials()
3854+{
3855+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/", 1);
3856+ QSignalSpy spy(&network, SIGNAL(buyItemSucceeded()));
3857+ network.buyItem("email", "password", "otp", "USD", "appid", "itemid", "paymentid", "backendid", true);
3858+ QTRY_COMPARE(spy.count(), 1);
3859+}
3860+
3861+void TestNetwork::testCheckAlreadyPurchased()
3862+{
3863+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/", 1);
3864+ QSignalSpy spy(&network, SIGNAL(buyItemSucceeded()));
3865+ network.checkItemPurchased("com.example.fakeapp", "");
3866+ QTRY_COMPARE(spy.count(), 1);
3867+}
3868+
3869+void TestNetwork::testCheckAlreadyPurchasedFail()
3870+{
3871+ setenv(PAY_BASE_URL_ENVVAR, "http://localhost:8000/notpurchased/", 1);
3872+ QSignalSpy spy(&network, SIGNAL(itemNotPurchased()));
3873+ network.checkItemPurchased("com.example.fakeapp", "");
3874+ // WTF DOES THIS HAPPEN TWICE
3875+ QTRY_COMPARE(spy.count(), 2);
3876+}
3877+
3878+void TestNetwork::testSanitizeUrl()
3879+{
3880+ QUrl url("https://example.com//test/this/heavily///really%2f/");
3881+ QUrl expected("https://example.com/test/this/heavily/really%2f/");
3882+ QString result = network.sanitizeUrl(url);
3883+ QTRY_COMPARE(result, expected.toString());
3884+}
3885+
3886+void TestNetwork::testEncodeQuerySlashes()
3887+{
3888+ QString query("abcdef/01235%3D");
3889+ QTRY_COMPARE(network.encodeQuerySlashes(query),
3890+ QStringLiteral("abcdef%2F01235%3D"));
3891+}
3892+
3893+void TestNetwork::cleanupTestCase()
3894+{
3895+ process->close();
3896+ process->deleteLater();
3897+}
3898+
3899+QTEST_MAIN(TestNetwork)
3900+#include "test_network.moc"
3901
3902=== added file 'pay-ui/pay-ui.in'
3903--- pay-ui/pay-ui.in 1970-01-01 00:00:00 +0000
3904+++ pay-ui/pay-ui.in 2016-03-11 14:49:24 +0000
3905@@ -0,0 +1,19 @@
3906+#!/bin/sh
3907+#
3908+# Copyright © 2016 Canonical Ltd.
3909+#
3910+# This program is free software: you can redistribute it and/or modify it
3911+# under the terms of the GNU General Public License version 3, as published
3912+# by the Free Software Foundation.
3913+#
3914+# This program is distributed in the hope that it will be useful, but
3915+# WITHOUT ANY WARRANTY; without even the implied warranties of
3916+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
3917+# PURPOSE. See the GNU General Public License for more details.
3918+#
3919+# You should have received a copy of the GNU General Public License along
3920+# with this program. If not, see <http://www.gnu.org/licenses/>.
3921+
3922+set -e
3923+
3924+env QML2_IMPORT_PATH=${QML2_IMPORT_PATH}:@QT_IMPORTS_DIR@ qmlscene @PAYUI_DIR@/payui.qml $@
3925
3926=== added directory 'pay-ui/tests'
3927=== added directory 'pay-ui/tests/autopilot'
3928=== added directory 'pay-ui/tests/autopilot/pay_ui'
3929=== added file 'pay-ui/tests/autopilot/pay_ui/__init__.py'
3930--- pay-ui/tests/autopilot/pay_ui/__init__.py 1970-01-01 00:00:00 +0000
3931+++ pay-ui/tests/autopilot/pay_ui/__init__.py 2016-03-11 14:49:24 +0000
3932@@ -0,0 +1,106 @@
3933+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
3934+#
3935+# Copyright (C) 2015 Canonical Ltd.
3936+#
3937+# This program is free software; you can redistribute it and/or modify
3938+# it under the terms of the GNU General Public License version 3, as published
3939+# by the Free Software Foundation.
3940+#
3941+# This program is distributed in the hope that it will be useful,
3942+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3943+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3944+# GNU General Public License for more details.
3945+#
3946+# You should have received a copy of the GNU General Public License
3947+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3948+
3949+import logging
3950+import ubuntuuitoolkit as uitk
3951+
3952+from autopilot import logging as autopilot_logging
3953+
3954+logger = logging.getLogger(__name__)
3955+
3956+
3957+class MainView(uitk.MainView):
3958+
3959+ def _tap(self, objectName):
3960+ """Find a widget and tap on it."""
3961+ item = self.wait_select_single(objectName=objectName)
3962+ if item.enabled:
3963+ self.pointing_device.click_object(item)
3964+
3965+ def _type_text(self, objectName, text):
3966+ """Find a text widget and enter some text in it."""
3967+ item = self.wait_select_single(
3968+ uitk.TextField, objectName=objectName)
3969+ item.write(text)
3970+
3971+ @autopilot_logging.log_action(logger.info)
3972+ def cancel(self):
3973+ """Tap on the 'Cancel' button."""
3974+ self._tap('cancelButton')
3975+
3976+ @autopilot_logging.log_action(logger.info)
3977+ def buy(self):
3978+ """Tap on the 'Buy Now' button."""
3979+ self._tap('buyButton')
3980+
3981+ @autopilot_logging.log_action(logger.info)
3982+ def enter_password(self, password):
3983+ """Type the password into the entry field."""
3984+ self._type_text('passwordField', password)
3985+
3986+ @autopilot_logging.log_action(logger.info)
3987+ def tap_on_webview(self):
3988+ """Tap in the center of the web view."""
3989+ self._tap('webView')
3990+
3991+ @autopilot_logging.log_action(logger.info)
3992+ def open_add_card_page(self):
3993+ """Open the 'Add Credit/Debit Card' page.
3994+
3995+ :return the Add Credit/Debit Card page.
3996+ """
3997+ self._tap('addCreditCardLabel')
3998+ return self.wait_select_single(objectName='pageWebkit')
3999+
4000+ @autopilot_logging.log_action(logger.info)
4001+ def get_payment_types(self):
4002+ """Get the payment types option selector widget.
4003+
4004+ :return the Payment Types option selector widget.
4005+ """
4006+ return self.wait_select_single(objectName='paymentTypes')
4007+
4008+ @autopilot_logging.log_action(logger.info)
4009+ def get_checkout_page(self):
4010+ """Get the widget for the Checkout page.
4011+
4012+ :return the Checkout page widget."""
4013+ return self.wait_select_single(objectName='pageCheckout')
4014+
4015+ @autopilot_logging.log_action(logger.info)
4016+ def tap_dialog_ok_button(self):
4017+ """Tap the 'OK' button in the dialog."""
4018+ self._tap('dialogOkButton')
4019+
4020+ @autopilot_logging.log_action(logger.info)
4021+ def tap_dialog_cancel_button(self):
4022+ """Tap the 'Cancel' button in the dialog."""
4023+ self._tap('dialogCancelButton')
4024+
4025+ @autopilot_logging.log_action(logger.info)
4026+ def tap_dialog_leave_button(self):
4027+ """Tap the 'Leave' button in the dialog."""
4028+ self._tap('leaveButton')
4029+
4030+ @autopilot_logging.log_action(logger.info)
4031+ def tap_dialog_stay_button(self):
4032+ """Tap the 'Stay' button in the dialog."""
4033+ self._tap('stayButton')
4034+
4035+ @autopilot_logging.log_action(logger.info)
4036+ def input_dialog_text(self, text):
4037+ """Type the text into the dialog text entry field."""
4038+ self._type_text('dialogInput', text)
4039
4040=== added directory 'pay-ui/tests/autopilot/pay_ui/tests'
4041=== added file 'pay-ui/tests/autopilot/pay_ui/tests/__init__.py'
4042--- pay-ui/tests/autopilot/pay_ui/tests/__init__.py 1970-01-01 00:00:00 +0000
4043+++ pay-ui/tests/autopilot/pay_ui/tests/__init__.py 2016-03-11 14:49:24 +0000
4044@@ -0,0 +1,77 @@
4045+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
4046+#
4047+# Copyright (C) 2014-2016 Canonical Ltd.
4048+#
4049+# This program is free software; you can redistribute it and/or modify
4050+# it under the terms of the GNU General Public License version 3, as published
4051+# by the Free Software Foundation.
4052+#
4053+# This program is distributed in the hope that it will be useful,
4054+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4055+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4056+# GNU General Public License for more details.
4057+#
4058+# You should have received a copy of the GNU General Public License
4059+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4060+
4061+import fixtures
4062+import json
4063+import os
4064+import shutil
4065+import subprocess
4066+import tempfile
4067+import ubuntuuitoolkit as uitk
4068+
4069+import pay_ui
4070+
4071+
4072+class BasePayUITestCase(uitk.base.UbuntuUIToolkitAppTestCase):
4073+ """Base Autopilot test case for the Pay UI project."""
4074+
4075+ dirpath = None
4076+
4077+ def setUp(self):
4078+ super().setUp()
4079+ build_dir = os.environ.get('BUILD_DIR', None)
4080+ source_dir = os.environ.get('SOURCE_DIR', None)
4081+ self.app = self.launch_application(build_dir, source_dir)
4082+
4083+ def create_config_dir(self):
4084+ self.dirpath = tempfile.mkdtemp()
4085+ self.useFixture(fixtures.EnvironmentVariable(
4086+ 'XDG_DATA_HOME', self.dirpath))
4087+
4088+ def clean_config_dir(self):
4089+ if self.dirpath:
4090+ shutil.rmtree(self.dirpath)
4091+
4092+ def launch_application(self, build_dir=None, source_dir=None):
4093+ if build_dir is None:
4094+ return self.launch_installed_app()
4095+ else:
4096+ return self.launch_built_application(build_dir, source_dir)
4097+
4098+ def launch_installed_app(self):
4099+ return self.launch_test_application(
4100+ '/usr/lib/payui/pay-ui',
4101+ app_type='qt',
4102+ emulator_base=uitk.UbuntuUIToolkitCustomProxyObjectBase)
4103+
4104+ def launch_built_application(self, build_dir, source_dir):
4105+ built_import_path = os.path.join(build_dir, 'backend')
4106+ self.useFixture(
4107+ fixtures.EnvironmentVariable(
4108+ 'QML2_IMPORT_PATH', newvalue=built_import_path))
4109+ main_qml_path = os.path.join(source_dir, 'app', 'payui.qml')
4110+ return self.launch_test_application(
4111+ uitk.base.get_qmlscene_launch_command(),
4112+ main_qml_path,
4113+ '--transparent',
4114+ 'purchase://com.example.testapp',
4115+ app_type='qt',
4116+ emulator_base=uitk.UbuntuUIToolkitCustomProxyObjectBase)
4117+
4118+ @property
4119+ def main_view(self):
4120+ """Return main view"""
4121+ return self.app.select_single(pay_ui.MainView)
4122
4123=== added file 'pay-ui/tests/autopilot/pay_ui/tests/mock_server.py'
4124--- pay-ui/tests/autopilot/pay_ui/tests/mock_server.py 1970-01-01 00:00:00 +0000
4125+++ pay-ui/tests/autopilot/pay_ui/tests/mock_server.py 2016-03-11 14:49:24 +0000
4126@@ -0,0 +1,337 @@
4127+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
4128+#
4129+# Copyright (C) 2014 Canonical Ltd.
4130+#
4131+# This program is free software; you can redistribute it and/or modify
4132+# it under the terms of the GNU General Public License version 3, as published
4133+# by the Free Software Foundation.
4134+#
4135+# This program is distributed in the hope that it will be useful,
4136+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4137+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4138+# GNU General Public License for more details.
4139+#
4140+# You should have received a copy of the GNU General Public License
4141+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4142+
4143+import json
4144+import socket
4145+import threading
4146+from http.server import BaseHTTPRequestHandler, HTTPServer
4147+
4148+
4149+html_success = """
4150+<html>
4151+ <body bgcolor="green" onClick="window.location.assign('/paymentmethods/completeadd')">
4152+ <h1>Placeholder for web interaction</h1>
4153+ <p>Click anywhere to proceed</p>
4154+ </body>
4155+</html>"
4156+"""
4157+
4158+html_cancel = """
4159+<html>
4160+ <body bgcolor="red" onClick="window.location.assign('/api/2.0/click/cancelled')">
4161+ <h1>Placeholder for web interaction</h1>
4162+ <p>Click anywhere to cancel</p>
4163+ </body>
4164+</html>"
4165+"""
4166+
4167+html_completed = """
4168+<html>
4169+ <body onLoad="window.location.assign('/click/succeeded')">
4170+ </body>
4171+</html>
4172+"""
4173+
4174+html_beforeunload = """
4175+<html>
4176+ <script language="javascript">
4177+ window.onbeforeunload = function() {
4178+ return 'Really want to add your card?'
4179+ }
4180+ window.onload = function() {
4181+ window.location.assign('/click/cancelled')
4182+ }
4183+ window.onclick = function() {
4184+ window.onbeforeunload = null;
4185+ window.location.assign('/paymentmethods/completeadd')
4186+ }
4187+ </script>
4188+ <body bgcolor="yellow">
4189+ <h1>Placeholder for web interaction</h1>
4190+ <p>Click 'Stay' and then anywhere to proceed.</p>
4191+ </body>
4192+</html>
4193+"""
4194+
4195+html_alert = """
4196+<html>
4197+ <body onClick="window.location.assign('/paymentmethods/completeadd')"
4198+ onLoad="alert('Click OK to add your card.')">
4199+ <h1>Placeholder for web interaction</h1>
4200+ <p>Click anywhere to proceed</p>
4201+ </body>
4202+</html>
4203+"""
4204+
4205+html_confirm = """
4206+<html>
4207+ <script language="javascript">
4208+ window.onload = function() {
4209+ if (window.confirm('Do you want to add your card?')) {
4210+ window.location.assign('/paymentmethods/completeadd')
4211+ } else {
4212+ window.location.assign('/click/cancelled')
4213+ }
4214+ }
4215+ </script>
4216+ <body bgcolor="pink">
4217+ <h1>Placeholder for web interaction</h1>
4218+ <p>Click ok to add card, or cancel to not.</p>
4219+ </body>
4220+</html>
4221+"""
4222+
4223+html_prompt = """
4224+<html>
4225+ <script language="javascript">
4226+ window.onload = function() {
4227+ if (window.prompt('Speak friend and enter') == 'friend') {
4228+ window.location.assign('/paymentmethods/completeadd')
4229+ } else {
4230+ window.location.assign('/click/cancelled')
4231+ }
4232+ }
4233+ </script>
4234+ <body bgcolor="pink">
4235+ <h1>Placeholder for web interaction</h1>
4236+ <p>Type friend and ok to add card.</p>
4237+ </body>
4238+</html>
4239+"""
4240+
4241+
4242+class MyHandler(BaseHTTPRequestHandler):
4243+
4244+ def do_HEAD(self):
4245+ self.send_response(200)
4246+ self.send_header("X-Click-Token", "X-Click-Token")
4247+ self.end_headers()
4248+
4249+ def response_payment_types(self, fail=False):
4250+ if fail:
4251+ self.send_response(404)
4252+ else:
4253+ self.send_response(200)
4254+ self.send_header("Content-type", "application/json")
4255+ self.end_headers()
4256+ self.wfile.write(bytes(json.dumps(self.server.payment_types), 'UTF-8'))
4257+
4258+ def interaction_html(self):
4259+ return html_cancel if self.server.interaction_cancel else html_success
4260+
4261+ def response_cc_interaction(self, fail=False):
4262+ if fail:
4263+ self.send_response(404)
4264+ else:
4265+ self.send_response(200)
4266+ self.send_header("Content-type", "text/html")
4267+ self.end_headers()
4268+ self.wfile.write(bytes(self.interaction_html(), 'UTF-8'))
4269+
4270+ def response_payment_add(self, fail=False):
4271+ if fail:
4272+ self.send_response(404)
4273+ else:
4274+ self.send_response(200)
4275+ self.send_header("Content-type", "text/html")
4276+ self.end_headers()
4277+ test = self.path.split('/')[1]
4278+ if test.find('js_alert') != -1:
4279+ self.wrile.write(bytes(html_alert, 'UTF-8'))
4280+ elif test.find('js_beforeunload') != -1:
4281+ self.wfile.write(bytes(html_beforeunload, 'UTF-8'))
4282+ elif test.find('js_confirm') != -1:
4283+ self.wfile.write(bytes(html_confirm, 'UTF-8'))
4284+ elif test.find('js_prompt') != -1:
4285+ self.wfile.write(bytes(html_prompt, 'UTF-8'))
4286+ else:
4287+ self.wfile.write(bytes(self.interaction_html(), 'UTF-8'))
4288+
4289+ def response_payment_add_complete(self):
4290+ """Add the new payment info to the list."""
4291+ self.server.payment_types[1]["choices"].append({
4292+ "currencies": ["USD"],
4293+ "id": 1999,
4294+ "requires_interaction": False,
4295+ "preferred": False,
4296+ "description": "Yet another payment method"
4297+ })
4298+ self.send_response(200)
4299+ self.send_header("Content-type", "text/html")
4300+ self.end_headers()
4301+ self.wfile.write(bytes(html_completed, 'UTF-8'))
4302+
4303+ def response_buy_item(self):
4304+ response = {
4305+ "state": "Complete",
4306+ }
4307+ if self.server.buy_cc_interaction:
4308+ response["state"] = "InProgress"
4309+ response["redirect_to"] = self.server.buy_cc_interaction
4310+ if self.server.fail:
4311+ response = {}
4312+ self.send_response(200)
4313+ self.send_header("Content-type", "application/json")
4314+ self.end_headers()
4315+ try:
4316+ self.wfile.write(bytes(json.dumps(response), 'UTF-8'))
4317+ except socket.error:
4318+ pass
4319+
4320+ def response_update_credentials(self, fail=False):
4321+ response = {
4322+ "token_key": "token_key",
4323+ "token_secret": "token_secret",
4324+ "consumer_key": "consumer_key",
4325+ "consumer_secret": "consumer_secret",
4326+ }
4327+ if fail:
4328+ self.send_response(404)
4329+ else:
4330+ self.send_response(200)
4331+ self.send_header("Content-type", "application/json")
4332+ self.end_headers()
4333+ self.wfile.write(bytes(json.dumps(response), 'UTF-8'))
4334+
4335+ def response_item_info(self, fail=False):
4336+ response = {
4337+ "title": "title",
4338+ "publisher": "publisher",
4339+ "icon_url": "icon_url",
4340+ "prices": {
4341+ "USD": 2.99,
4342+ "EUR": 2.49,
4343+ "GBP": 1.99,
4344+ },
4345+ }
4346+ if fail:
4347+ self.send_response(404)
4348+ else:
4349+ self.send_response(200)
4350+ self.send_header("Content-type", "application/json")
4351+ self.end_headers()
4352+ self.wfile.write(bytes(json.dumps(response), 'UTF-8'))
4353+
4354+ def response_auth_error(self):
4355+ self.send_response(401)
4356+ self.send_header("Content-type", "application/json")
4357+ self.end_headers()
4358+ self.wfile.write(bytes(json.dumps(dict()), 'UTF-8'))
4359+
4360+ def do_POST(self):
4361+ """Respond to a POST request."""
4362+ #content = self.rfile.read(int(self.headers.get('content-length')))
4363+ #structure = json.loads(content.decode('utf-8'))
4364+ if self.path.find("purchases/") != -1:
4365+ self.response_buy_item()
4366+
4367+ def do_GET(self):
4368+ """Respond to a GET request."""
4369+ fail = self.path.find("/fail/") != -1
4370+ if self.path.find("paymentmethods/add/") != -1:
4371+ self.response_payment_add()
4372+ elif self.path.find("paymentmethods/completeadd") != -1:
4373+ self.response_payment_add_complete()
4374+ elif self.path.find("paymentmethods/") != -1:
4375+ self.response_payment_types(fail)
4376+ elif self.path.find("creditcard_interaction/") != -1:
4377+ self.response_cc_interaction()
4378+ elif self.path.find("purchases/") != -1:
4379+ self.response_buy_item()
4380+ elif self.path.find("creds/") != -1 or self.path.find("wallet/") != -1:
4381+ self.response_update_credentials(fail)
4382+ elif self.path.find("iteminfo/") != -1:
4383+ self.response_item_info(fail)
4384+ elif self.path.find("/authError/") != -1:
4385+ self.response_auth_error()
4386+
4387+
4388+def initial_payment_types():
4389+ return [
4390+ {
4391+ "description": "PayPal",
4392+ "id": "paypal",
4393+ "preferred": False,
4394+ "choices": [
4395+ {
4396+ "currencies": [
4397+ "USD",
4398+ "GBP",
4399+ "EUR"
4400+ ],
4401+ "id": 532,
4402+ "requires_interaction": False,
4403+ "preferred": False,
4404+ "description": ("PayPal Preapproved Payment "
4405+ "(exp. 2014-04-12)")
4406+ }
4407+ ]
4408+ },
4409+ {
4410+ "description": "Credit or Debit Card",
4411+ "id": "credit_card",
4412+ "preferred": True,
4413+ "choices": [
4414+ {
4415+ "currencies": [
4416+ "USD"
4417+ ],
4418+ "id": 1767,
4419+ "requires_interaction": False,
4420+ "preferred": False,
4421+ "description": ("**** **** **** 1111 "
4422+ "(Visa, exp. 02/2015)")
4423+ },
4424+ {
4425+ "currencies": [
4426+ "USD"
4427+ ],
4428+ "id": 1726,
4429+ "requires_interaction": False,
4430+ "preferred": True,
4431+ "description": ("**** **** **** 1111 "
4432+ "(Visa, exp. 03/2015)")
4433+ }
4434+ ]
4435+ }
4436+ ]
4437+
4438+
4439+class MockServer:
4440+ def __init__(self):
4441+ server_address = ('', 0)
4442+ self.server = HTTPServer(server_address, MyHandler)
4443+ tcp_port = self.server.socket.getsockname()[1]
4444+ self.base_url = "http://127.0.0.1:%d/" % tcp_port
4445+ server_thread = threading.Thread(target=self.server.serve_forever)
4446+ server_thread.start()
4447+ self.server.payment_types = initial_payment_types()
4448+ self.server.fail = False
4449+ self.server.buy_cc_interaction = None
4450+ self.server.interaction_cancel = False
4451+
4452+ def set_purchase_needs_cc_interaction(self):
4453+ # Real server returns path starting with / here, not full URL.
4454+ self.server.buy_cc_interaction = "/creditcard_interaction"
4455+
4456+ def set_interaction_result_cancelled(self):
4457+ self.server.interaction_cancel = True
4458+
4459+ def url(self, tail=""):
4460+ return self.base_url + tail
4461+
4462+ def shutdown(self):
4463+ self.server.shutdown()
4464
4465=== added file 'pay-ui/tests/autopilot/pay_ui/tests/test_pay_ui.py'
4466--- pay-ui/tests/autopilot/pay_ui/tests/test_pay_ui.py 1970-01-01 00:00:00 +0000
4467+++ pay-ui/tests/autopilot/pay_ui/tests/test_pay_ui.py 2016-03-11 14:49:24 +0000
4468@@ -0,0 +1,186 @@
4469+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
4470+#
4471+# Copyright (C) 2014-2016 Canonical Ltd.
4472+#
4473+# This program is free software; you can redistribute it and/or modify
4474+# it under the terms of the GNU General Public License version 3, as published
4475+# by the Free Software Foundation.
4476+#
4477+# This program is distributed in the hope that it will be useful,
4478+# but WITHOUT ANY WARRANTY; without even the implied warranty of
4479+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4480+# GNU General Public License for more details.
4481+#
4482+# You should have received a copy of the GNU General Public License
4483+# along with this program. If not, see <http://www.gnu.org/licenses/>.
4484+
4485+import fixtures
4486+import testtools
4487+
4488+from testtools.matchers import Equals, NotEquals
4489+from autopilot.matchers import Eventually
4490+
4491+from pay_ui import tests
4492+from pay_ui.tests import mock_server
4493+
4494+
4495+class PayUITestCase(tests.BasePayUITestCase):
4496+
4497+ def setUp(self):
4498+ self.clean_config_dir()
4499+ self.mock_server = mock_server.MockServer()
4500+ self.addCleanup(self.mock_server.shutdown)
4501+ self.useFixture(fixtures.EnvironmentVariable(
4502+ 'PAY_BASE_URL',
4503+ self.mock_server.url() + '/' + self.id().split('.')[-1]))
4504+ self.useFixture(fixtures.EnvironmentVariable(
4505+ 'U1_SEARCH_BASE_URL', self.mock_server.url('iteminfo/')))
4506+ self.useFixture(fixtures.EnvironmentVariable(
4507+ 'SSO_AUTH_BASE_URL', self.mock_server.url('login/')))
4508+ self.useFixture(fixtures.EnvironmentVariable('GET_CREDENTIALS', '0'))
4509+ self.create_config_dir()
4510+ self.addCleanup(self.clean_config_dir)
4511+ super().setUp()
4512+
4513+ def app_returncode(self):
4514+ return self.app.process.wait(timeout=30)
4515+
4516+ def test_ui_initialized(self):
4517+ main = self.main_view
4518+ self.assertThat(main, NotEquals(None))
4519+
4520+ def test_cancel_purchase(self):
4521+ self.main_view.cancel()
4522+ self.assertThat(self.app_returncode(), Equals(1))
4523+
4524+ def test_basic_purchase(self):
4525+ self.skipTest('Mouse clicks on buyButton not registering.')
4526+ self.main_view.enter_password('password123')
4527+ self.main_view.buy()
4528+ self.assertThat(self.app_returncode(), Equals(0))
4529+
4530+ def test_add_credit_card_completed(self):
4531+ payment_types = self.main_view.get_payment_types()
4532+ self.assertThat(payment_types.get_option_count(), Equals(3))
4533+ self.main_view.open_add_card_page()
4534+ self.take_screenshot('add_card_page')
4535+ self.main_view.tap_on_webview()
4536+ self.assertThat(payment_types.get_option_count, Eventually(Equals(4)))
4537+
4538+ def test_add_credit_card_returns_on_cancel(self):
4539+ self.mock_server.set_interaction_result_cancelled()
4540+ payment_types = self.main_view.get_payment_types()
4541+ self.assertThat(payment_types.get_option_count(), Equals(3))
4542+ self.main_view.open_add_card_page()
4543+ self.take_screenshot('add_card_page')
4544+ self.main_view.tap_on_webview()
4545+ checkout_page = self.main_view.get_checkout_page()
4546+ self.assertThat(checkout_page.get_properties()["active"],
4547+ Eventually(Equals(True)))
4548+
4549+ def test_add_credit_card_cancelled(self):
4550+ self.mock_server.set_interaction_result_cancelled()
4551+ payment_types = self.main_view.get_payment_types()
4552+ self.assertThat(payment_types.get_option_count(), Equals(3))
4553+ self.main_view.open_add_card_page()
4554+ self.take_screenshot('add_card_page')
4555+ self.main_view.tap_on_webview()
4556+ self.assertThat(payment_types.get_option_count, Eventually(Equals(3)))
4557+
4558+ @testtools.skip('JS Alert dialog seems to not work.')
4559+ def test_add_credit_card_js_alert(self):
4560+ payment_types = self.main_view.get_payment_types()
4561+ self.assertThat(payment_types.get_option_count(), Equals(3))
4562+ self.main_view.open_add_card_page()
4563+ self.take_screenshot('add_card_page')
4564+ self.main_view.tap_dialog_ok_button()
4565+ self.assertThat(payment_types.get_option_count, Eventually(Equals(4)))
4566+
4567+ def test_add_credit_card_js_beforeunload(self):
4568+ payment_types = self.main_view.get_payment_types()
4569+ self.assertThat(payment_types.get_option_count(), Equals(3))
4570+ self.main_view.open_add_card_page()
4571+ self.take_screenshot('add_card_page')
4572+ self.main_view.tap_dialog_stay_button()
4573+ self.main_view.tap_on_webview()
4574+ self.assertThat(payment_types.get_option_count, Eventually(Equals(4)))
4575+
4576+ @testtools.skip('Clicking add card link second seems to not work.')
4577+ def test_add_credit_card_js_beforeunload_twice(self):
4578+ payment_types = self.main_view.get_payment_types()
4579+ self.assertThat(payment_types.get_option_count(), Equals(3))
4580+ self.main_view.open_add_card_page()
4581+ self.take_screenshot('add_card_page')
4582+ self.main_view.tap_dialog_leave_button()
4583+ self.take_screenshot('beforeunload_left')
4584+ self.main_view.open_add_card_page()
4585+ self.take_screenshot('beforeunload_back')
4586+ self.main_view.tap_dialog_stay_button()
4587+ self.main_view.tap_on_web_view()
4588+ self.assertThat(payment_types.get_option_count, Eventually(Equals(4)))
4589+
4590+ def test_add_credit_card_js_beforeunload_cancelled(self):
4591+ payment_types = self.main_view.get_payment_types()
4592+ self.assertThat(payment_types.get_option_count(), Equals(3))
4593+ self.main_view.open_add_card_page()
4594+ self.take_screenshot('add_card_page')
4595+ self.main_view.tap_dialog_leave_button()
4596+ self.take_screenshot('beforeunload_cancelled')
4597+ self.assertThat(payment_types.get_option_count, Eventually(Equals(3)))
4598+
4599+ def test_add_credit_card_js_confirm_ok(self):
4600+ payment_types = self.main_view.get_payment_types()
4601+ self.assertThat(payment_types.get_option_count(), Equals(3))
4602+ self.main_view.open_add_card_page()
4603+ self.take_screenshot('add_card_page')
4604+ self.main_view.tap_dialog_ok_button()
4605+ self.assertThat(payment_types.get_option_count, Eventually(Equals(4)))
4606+
4607+ def test_add_credit_card_js_confirm_cancel(self):
4608+ payment_types = self.main_view.get_payment_types()
4609+ self.assertThat(payment_types.get_option_count(), Equals(3))
4610+ self.main_view.open_add_card_page()
4611+ self.take_screenshot('add_card_page')
4612+ self.main_view.tap_dialog_cancel_button()
4613+ self.assertThat(payment_types.get_option_count, Eventually(Equals(3)))
4614+
4615+ def test_add_credit_card_js_prompt_ok(self):
4616+ payment_types = self.main_view.get_payment_types()
4617+ self.assertThat(payment_types.get_option_count(), Equals(3))
4618+ self.main_view.open_add_card_page()
4619+ self.take_screenshot('add_card_page')
4620+ self.main_view.input_dialog_text('friend')
4621+ self.main_view.tap_dialog_ok_button()
4622+ self.assertThat(payment_types.get_option_count, Eventually(Equals(4)))
4623+
4624+ def test_add_credit_card_js_prompt_ok_wrong_text(self):
4625+ payment_types = self.main_view.get_payment_types()
4626+ self.assertThat(payment_types.get_option_count(), Equals(3))
4627+ self.main_view.open_add_card_page()
4628+ self.take_screenshot('add_card_page')
4629+ self.main_view.input_dialog_text('amigo')
4630+ self.main_view.tap_dialog_ok_button()
4631+ self.assertThat(payment_types.get_option_count, Eventually(Equals(3)))
4632+
4633+ def test_add_credit_card_js_prompt_cancel(self):
4634+ payment_types = self.main_view.get_payment_types()
4635+ self.assertThat(payment_types.get_option_count(), Equals(3))
4636+ self.main_view.open_add_card_page()
4637+ self.take_screenshot('add_card_page')
4638+ self.main_view.tap_dialog_cancel_button()
4639+ self.assertThat(payment_types.get_option_count, Eventually(Equals(3)))
4640+
4641+ def test_purchase_with_web_interaction_completed(self):
4642+ self.skipTest('Mouse clicks on buyButton not registering.')
4643+ self.mock_server.set_purchase_needs_cc_interaction()
4644+ self.main_view.buy()
4645+ self.main_view.tap_on_webview()
4646+ self.assertThat(self.app_returncode(), Equals(0))
4647+
4648+ def test_purchase_with_web_interaction_cancelled(self):
4649+ self.skipTest('Mouse clicks on buyButton not registering.')
4650+ self.mock_server.set_purchase_needs_cc_interaction()
4651+ self.mock_server.set_interaction_result_cancelled()
4652+ self.main_view.buy()
4653+ self.main_view.tap_on_webview()
4654+ self.assertThat(self.app_returncode(), Equals(1))
4655
4656=== added file 'pay-ui/tests/autopilot/run_autopilot'
4657--- pay-ui/tests/autopilot/run_autopilot 1970-01-01 00:00:00 +0000
4658+++ pay-ui/tests/autopilot/run_autopilot 2016-03-11 14:49:24 +0000
4659@@ -0,0 +1,1 @@
4660+GET_CREDENTIALS=0 autopilot3 run -v pay_ui
4661
4662=== modified file 'po/CMakeLists.txt'
4663--- po/CMakeLists.txt 2015-12-11 19:49:20 +0000
4664+++ po/CMakeLists.txt 2016-03-11 14:49:24 +0000
4665@@ -15,7 +15,7 @@
4666 # Creates the .pot file containing the translations template
4667 set(INTLTOOL_ENV
4668 XGETTEXT="${GETTEXT_XGETTEXT_EXECUTABLE}"
4669- XGETTEXT_ARGS="--keyword=Gettext;--keyword=NGettext:1,2"
4670+ XGETTEXT_ARGS="--keyword=Gettext;--keyword=NGettext:1,2;--keyword=tr;--keyword=tr:1,2;--keyword=N_"
4671 srcdir="${CMAKE_CURRENT_SOURCE_DIR}"
4672 )
4673 add_custom_target(${POT_FILE}
4674
4675=== modified file 'po/POTFILES.in'
4676--- po/POTFILES.in 2015-12-11 19:49:20 +0000
4677+++ po/POTFILES.in 2016-03-11 14:49:24 +0000
4678@@ -1,1 +1,9 @@
4679+pay-ui/app/components/ConfirmDialog.qml
4680+pay-ui/app/components/PromptDialog.qml
4681+pay-ui/app/components/BeforeUnloadDialog.qml
4682+pay-ui/app/components/SecurityCertificatePopover.qml
4683+pay-ui/app/components/AlertDialog.qml
4684+pay-ui/app/ui/CheckoutPage.qml
4685+pay-ui/app/ui/ErrorDialog.qml
4686+pay-ui/app/payui.qml
4687 service-ng/src/pay-service-2/service/pay_service.go
4688
4689=== added file 'po/aa.po'
4690--- po/aa.po 1970-01-01 00:00:00 +0000
4691+++ po/aa.po 2016-03-11 14:49:24 +0000
4692@@ -0,0 +1,106 @@
4693+# Afar translation for pay-ui
4694+# Copyright (c) 2015 Rosetta Contributors and Canonical Ltd 2015
4695+# This file is distributed under the same license as the pay-ui package.
4696+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
4697+#
4698+msgid ""
4699+msgstr ""
4700+"Project-Id-Version: pay-service\n"
4701+"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
4702+"POT-Creation-Date: 2015-02-19 15:01-0500\n"
4703+"PO-Revision-Date: 2015-04-26 13:56+0000\n"
4704+"Last-Translator: Charif AYFARAH <ayfarah@ymail.com>\n"
4705+"Language-Team: Afar <aa@li.org>\n"
4706+"MIME-Version: 1.0\n"
4707+"Content-Type: text/plain; charset=UTF-8\n"
4708+"Content-Transfer-Encoding: 8bit\n"
4709+"X-Launchpad-Export-Date: 2015-12-25 05:38+0000\n"
4710+"X-Generator: Launchpad (build 17865)\n"
4711+
4712+#: ../app/payui.qml:148
4713+msgid "Finish Purchase"
4714+msgstr "Limok gaba kal"
4715+
4716+#: ../app/payui.qml:163
4717+msgid "Incorrect Password, please try again."
4718+msgstr "Cuumiti makot yan, maganak kaadu gabbat."
4719+
4720+#: ../app/payui.qml:276
4721+msgid "Purchase failed"
4722+msgstr "Limo makkinna"
4723+
4724+#: ../app/payui.qml:277
4725+msgid "The purchase couldn't be completed."
4726+msgstr "limo ma duuduminna."
4727+
4728+#: ../app/payui.qml:292
4729+msgid "Error contacting the server"
4730+msgstr "Yayfaayi angaaraw soka le"
4731+
4732+#: ../app/payui.qml:293 ../app/payui.qml:309
4733+msgid "Do you want to try again?"
4734+msgstr "Kaadu gabbattam maay faxxa?"
4735+
4736+#: ../app/payui.qml:308
4737+msgid "Adding Credit Card failed"
4738+msgstr "Abuúd Karti edde osisaanam makkinna"
4739+
4740+#: ../app/payui.qml:327
4741+msgid "Processing Purchase"
4742+msgstr "Limô gexso"
4743+
4744+#: ../app/payui.qml:327
4745+msgid "Loading"
4746+msgstr "Qulluumah"
4747+
4748+#: ../app/payui.qml:328
4749+msgid "Please wait…"
4750+msgstr "Maganak qambal..."
4751+
4752+#: ../app/payui.qml:342 ../app/ui/CheckoutPage.qml:370
4753+msgid "Cancel"
4754+msgstr "Bayis"
4755+
4756+#: ../app/payui.qml:401
4757+msgid "Add Payment"
4758+msgstr "Mekla edde osis"
4759+
4760+#: ../app/ui/CheckoutPage.qml:27
4761+msgid "Payment"
4762+msgstr "Mekla"
4763+
4764+#: ../app/ui/CheckoutPage.qml:228
4765+msgid "Enter your Ubuntu One password"
4766+msgstr "Ubuntu One cuumita culus"
4767+
4768+#: ../app/ui/CheckoutPage.qml:271
4769+msgid "Type your verification code:"
4770+msgstr "Mudenti diggoyso culus:"
4771+
4772+#: ../app/ui/CheckoutPage.qml:282
4773+msgid "2-factor device code"
4774+msgstr "2-afeetah aalatih mudenta"
4775+
4776+#: ../app/ui/CheckoutPage.qml:379
4777+msgid "Buy Now"
4778+msgstr "Away xaamit"
4779+
4780+#: ../app/ui/CheckoutPage.qml:393
4781+msgid "Add credit/debit card"
4782+msgstr "Abuú/Raci karti edde osis"
4783+
4784+#: ../app/ui/ErrorDialog.qml:45
4785+msgid "Retry"
4786+msgstr "Qagis"
4787+
4788+#: ../app/ui/ErrorDialog.qml:55
4789+msgid "Close"
4790+msgstr "Alif"
4791+
4792+#: payui_payui.desktop.in.in.h:1
4793+msgid "Pay UI"
4794+msgstr "UI mekel"
4795+
4796+#: payui_payui.desktop.in.in.h:2
4797+msgid "The application for completing a purchase."
4798+msgstr "Limô duddoh abnisso"
4799
4800=== added file 'po/am.po'
4801--- po/am.po 1970-01-01 00:00:00 +0000
4802+++ po/am.po 2016-03-11 14:49:24 +0000
4803@@ -0,0 +1,106 @@
4804+# Amharic translation for pay-ui
4805+# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
4806+# This file is distributed under the same license as the pay-ui package.
4807+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
4808+#
4809+msgid ""
4810+msgstr ""
4811+"Project-Id-Version: pay-service\n"
4812+"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
4813+"POT-Creation-Date: 2015-02-19 15:01-0500\n"
4814+"PO-Revision-Date: 2015-03-03 14:18+0000\n"
4815+"Last-Translator: samson <Unknown>\n"
4816+"Language-Team: Amharic <am@li.org>\n"
4817+"MIME-Version: 1.0\n"
4818+"Content-Type: text/plain; charset=UTF-8\n"
4819+"Content-Transfer-Encoding: 8bit\n"
4820+"X-Launchpad-Export-Date: 2015-12-25 05:38+0000\n"
4821+"X-Generator: Launchpad (build 17865)\n"
4822+
4823+#: ../app/payui.qml:148
4824+msgid "Finish Purchase"
4825+msgstr "ግዢውን መጨረሻ"
4826+
4827+#: ../app/payui.qml:163
4828+msgid "Incorrect Password, please try again."
4829+msgstr "የተሳሳተ የመግቢያ ቃል እባክዎን እንደገና ይሞክሩ"
4830+
4831+#: ../app/payui.qml:276
4832+msgid "Purchase failed"
4833+msgstr "ግዢው አልተሳካም"
4834+
4835+#: ../app/payui.qml:277
4836+msgid "The purchase couldn't be completed."
4837+msgstr "ግዢውን መፈጸም አልተቻለም"
4838+
4839+#: ../app/payui.qml:292
4840+msgid "Error contacting the server"
4841+msgstr "ስህተት ተፈጥሯል ሰርቨሩን በመገናኘት ላይ እንዳለ"
4842+
4843+#: ../app/payui.qml:293 ../app/payui.qml:309
4844+msgid "Do you want to try again?"
4845+msgstr "እንደገና መሞከር ይፈልጋሉ?"
4846+
4847+#: ../app/payui.qml:308
4848+msgid "Adding Credit Card failed"
4849+msgstr "የ ክሬዲር ካርድ መጨመር አልተቻለም"
4850+
4851+#: ../app/payui.qml:327
4852+msgid "Processing Purchase"
4853+msgstr "ግዢውን በማካሄድ ላይ"
4854+
4855+#: ../app/payui.qml:327
4856+msgid "Loading"
4857+msgstr "በመጫን ላይ"
4858+
4859+#: ../app/payui.qml:328
4860+msgid "Please wait…"
4861+msgstr "እባክዎን ይጠብቁ…"
4862+
4863+#: ../app/payui.qml:342 ../app/ui/CheckoutPage.qml:370
4864+msgid "Cancel"
4865+msgstr "መሰረዣ"
4866+
4867+#: ../app/payui.qml:401
4868+msgid "Add Payment"
4869+msgstr "ክፍያውን መጨመሪያ"
4870+
4871+#: ../app/ui/CheckoutPage.qml:27
4872+msgid "Payment"
4873+msgstr "ክፍያ"
4874+
4875+#: ../app/ui/CheckoutPage.qml:228
4876+msgid "Enter your Ubuntu One password"
4877+msgstr "የ እርስዎን የ ኡቡንቱ ዋን የ መግቢያ ቃል ያስገቡ"
4878+
4879+#: ../app/ui/CheckoutPage.qml:271
4880+msgid "Type your verification code:"
4881+msgstr "የ እርስዎን ማረጋገጫ ኮድ ይጻፉ:"
4882+
4883+#: ../app/ui/CheckoutPage.qml:282
4884+msgid "2-factor device code"
4885+msgstr "2-ፋክተር የ አካል ኮድ"
4886+
4887+#: ../app/ui/CheckoutPage.qml:379
4888+msgid "Buy Now"
4889+msgstr "አሁን መግዣ"
4890+
4891+#: ../app/ui/CheckoutPage.qml:393
4892+msgid "Add credit/debit card"
4893+msgstr "ክሬዲት/ዴቢት ካርድ መጨመሪያ"
4894+
4895+#: ../app/ui/ErrorDialog.qml:45
4896+msgid "Retry"
4897+msgstr "እንደገና ይሞክሩ"
4898+
4899+#: ../app/ui/ErrorDialog.qml:55
4900+msgid "Close"
4901+msgstr "መዝጊያ"
4902+
4903+#: payui_payui.desktop.in.in.h:1
4904+msgid "Pay UI"
4905+msgstr "መክፈያ UI"
4906+
4907+#: payui_payui.desktop.in.in.h:2
4908+msgid "The application for completing a purchase."
4909+msgstr "መተግበሪያ ግዢውን ለመፈጸም"
4910
4911=== added file 'po/ast.po'
4912--- po/ast.po 1970-01-01 00:00:00 +0000
4913+++ po/ast.po 2016-03-11 14:49:24 +0000
4914@@ -0,0 +1,106 @@
4915+# Asturian translation for pay-ui
4916+# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
4917+# This file is distributed under the same license as the pay-ui package.
4918+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
4919+#
4920+msgid ""
4921+msgstr ""
4922+"Project-Id-Version: pay-service\n"
4923+"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
4924+"POT-Creation-Date: 2015-02-19 15:01-0500\n"
4925+"PO-Revision-Date: 2015-06-09 15:40+0000\n"
4926+"Last-Translator: enolp <enolp@softastur.org>\n"
4927+"Language-Team: Asturian <ast@li.org>\n"
4928+"MIME-Version: 1.0\n"
4929+"Content-Type: text/plain; charset=UTF-8\n"
4930+"Content-Transfer-Encoding: 8bit\n"
4931+"X-Launchpad-Export-Date: 2015-12-25 05:38+0000\n"
4932+"X-Generator: Launchpad (build 17865)\n"
4933+
4934+#: ../app/payui.qml:148
4935+msgid "Finish Purchase"
4936+msgstr "Acabar la compra"
4937+
4938+#: ../app/payui.qml:163
4939+msgid "Incorrect Password, please try again."
4940+msgstr "La contraseña ye incorreuta. Inténtalo de nueves."
4941+
4942+#: ../app/payui.qml:276
4943+msgid "Purchase failed"
4944+msgstr "Falló la compra"
4945+
4946+#: ../app/payui.qml:277
4947+msgid "The purchase couldn't be completed."
4948+msgstr "Nun pudo completase la compra"
4949+
4950+#: ../app/payui.qml:292
4951+msgid "Error contacting the server"
4952+msgstr "Fallu al contautar col sirvidor"
4953+
4954+#: ../app/payui.qml:293 ../app/payui.qml:309
4955+msgid "Do you want to try again?"
4956+msgstr "¿Quies intentalo otra vegada?"
4957+
4958+#: ../app/payui.qml:308
4959+msgid "Adding Credit Card failed"
4960+msgstr "Fallu al amestar tarxeta de creitu"
4961+
4962+#: ../app/payui.qml:327
4963+msgid "Processing Purchase"
4964+msgstr "Procesando la compra"
4965+
4966+#: ../app/payui.qml:327
4967+msgid "Loading"
4968+msgstr "Cargando"
4969+
4970+#: ../app/payui.qml:328
4971+msgid "Please wait…"
4972+msgstr "Espera, por favor..."
4973+
4974+#: ../app/payui.qml:342 ../app/ui/CheckoutPage.qml:370
4975+msgid "Cancel"
4976+msgstr "Encaboxar"
4977+
4978+#: ../app/payui.qml:401
4979+msgid "Add Payment"
4980+msgstr "Amestar pagu"
4981+
4982+#: ../app/ui/CheckoutPage.qml:27
4983+msgid "Payment"
4984+msgstr "Pagu"
4985+
4986+#: ../app/ui/CheckoutPage.qml:228
4987+msgid "Enter your Ubuntu One password"
4988+msgstr "Introduz la to contraseña d'Ubuntu One"
4989+
4990+#: ../app/ui/CheckoutPage.qml:271
4991+msgid "Type your verification code:"
4992+msgstr "Escribi'l códigu de comprobación:"
4993+
4994+#: ../app/ui/CheckoutPage.qml:282
4995+msgid "2-factor device code"
4996+msgstr "códigu de preséu 2-factor"
4997+
4998+#: ../app/ui/CheckoutPage.qml:379
4999+msgid "Buy Now"
5000+msgstr "Mercar Agora"
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: