1/****************************************************************************
2**
3** Copyright (C) 2015 The Qt Company Ltd.
4** Contact: http://www.qt.io/licensing/
5**
6** This file is part of the Purchasing module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL3-COMM$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see http://www.qt.io/terms-conditions. For further
15** information use the contact form at http://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPLv3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include "qinappproductqmltype_p.h"
30#include "qinappstoreqmltype_p.h"
31#include <QtPurchasing/qinapptransaction.h>
32#include <QtPurchasing/qinappstore.h>
33#include <QtCore/qcoreevent.h>
34
35QT_BEGIN_NAMESPACE
36
37/*!
38 \qmltype Product
39 \inqmlmodule QtPurchasing
40 \since QtPurchasing 1.0
41 \ingroup qtpurchasing
42 \brief A product for in-app purchasing.
43
44 Product contains information about a product in the external market place. Once
45 the product's \l identifier and \l type are set, the product will be queried from
46 the external market place. Properties such as \l price will then be set, and
47 it will be possible to purchase the product. The \l status property holds information
48 on the registration process.
49
50 \note It is not possible to change the identifier and type once they have both been set
51 and the product has been registered.
52*/
53
54QInAppProductQmlType::QInAppProductQmlType(QObject *parent)
55 : QObject(parent)
56 , m_status(Uninitialized)
57 , m_type(QInAppProductQmlType::ProductType(-1))
58 , m_componentComplete(false)
59 , m_store(0)
60 , m_product(0)
61{
62}
63
64/*!
65 \qmlproperty object QtPurchasing::Product::store
66
67 This property holds the store containing the product. When the product is created as
68 a child of the store, this is set automatically to the parent, as in the following
69 example:
70
71 \qml
72 Store {
73 Product {
74 // No need to set the store explicitly here, as it will automatically be
75 // bound to the parent
76 identifier: "myConsumableProduct"
77 type: Product.Consumable
78 }
79 Product {
80 // No need to set the store explicitly here, as it will automatically be
81 // bound to the parent
82 identifier: "myUnlockableProduct"
83 type: Product.Unlockable
84 }
85 }
86 \endqml
87
88 However, in some advanced use cases, for example when products are created based on
89 a model, it's also possible to create the product anywhere in the QML document
90 and set the store explicitly, like in the following example:
91
92 \code
93 ListModel {
94 id: productModel
95 ListElement {
96 productIdentifier: "myConsumableProduct"
97 productType: Product.Consumable
98 }
99 ListElement {
100 productIdentifier: "myUnlockableProduct"
101 productType: Product.Unlockable
102 }
103 }
104
105 Store {
106 id: myStore
107 }
108
109 Instantiator {
110 model: productModel
111 delegate: Product {
112 identifier: productIdentifier
113 type: productType
114 store: myStore
115 }
116 }
117 \endcode
118 */
119void QInAppProductQmlType::setStore(QInAppStoreQmlType *store)
120{
121 if (m_store == store)
122 return;
123
124 if (m_store != 0)
125 m_store->store()->disconnect(receiver: this);
126
127 m_store = store;
128 connect(sender: m_store->store(), signal: &QInAppStore::productRegistered,
129 receiver: this, slot: &QInAppProductQmlType::handleProductRegistered);
130 connect(sender: m_store->store(), signal: &QInAppStore::productUnknown,
131 receiver: this, slot: &QInAppProductQmlType::handleProductUnknown);
132 connect(sender: m_store->store(), signal: &QInAppStore::transactionReady,
133 receiver: this, slot: &QInAppProductQmlType::handleTransaction);
134
135 updateProduct();
136
137 emit storeChanged();
138}
139
140QInAppStoreQmlType *QInAppProductQmlType::store() const
141{
142 return m_store;
143}
144
145void QInAppProductQmlType::componentComplete()
146{
147 if (!m_componentComplete) {
148 m_componentComplete = true;
149 updateProduct();
150 }
151}
152
153/*!
154 \qmlproperty string QtPurchasing::Product::identifier
155 This property holds the identifier of the product in the external market place. It must match the
156 identifier used to register the product externally before-hand.
157
158 When both the identifier and \l type is set, the product is queried from the external market place,
159 and its other properties are updated asynchronously. At this point, the identifier and type
160 can no longer be changed.
161
162 The following example queries an unlockable product named "myUnlockableProduct" from the external
163 market place.
164 \qml
165 Store {
166 Product {
167 identifier: "myUnlockableProduct"
168 type: Product.Unlockable
169
170 // ...
171 }
172 }
173 \endqml
174*/
175void QInAppProductQmlType::setIdentifier(const QString &identifier)
176{
177 if (m_identifier == identifier)
178 return;
179
180 if (m_status != Uninitialized) {
181 qWarning(msg: "A product's identifier cannot be changed once the product has been initialized.");
182 return;
183 }
184
185 m_identifier = identifier;
186 if (m_componentComplete)
187 updateProduct();
188 emit identifierChanged();
189}
190
191void QInAppProductQmlType::updateProduct()
192{
193 if (m_store == 0)
194 return;
195
196 Status oldStatus = m_status;
197 QInAppProduct *product = 0;
198 if (m_identifier.isEmpty() || m_type == QInAppProductQmlType::ProductType(-1)) {
199 m_status = Uninitialized;
200 } else {
201 product = m_store->store()->registeredProduct(identifier: m_identifier);
202 if (product != 0 && product == m_product)
203 return;
204
205 if (product == 0) {
206 m_status = PendingRegistration;
207 m_store->store()->registerProduct(productType: QInAppProduct::ProductType(m_type), identifier: m_identifier);
208 } else if (product->productType() != QInAppProduct::ProductType(m_type)) {
209 qWarning(msg: "Product registered multiple times with different product types.");
210 product = 0;
211 m_status = Uninitialized;
212 } else {
213 m_status = Registered;
214 }
215 }
216
217 setProduct(product);
218 if (oldStatus != m_status)
219 emit statusChanged();
220}
221
222/*!
223 \qmlmethod QtPurchasing::Product::resetStatus()
224
225 Resets the \l status of this product and retries querying it from the external
226 market place.
227
228 This method can be used when querying the product failed for some reason
229 (such as network timeouts).
230
231 \since QtPurchasing 1.0.2
232*/
233void QInAppProductQmlType::resetStatus()
234{
235 updateProduct();
236}
237
238QString QInAppProductQmlType::identifier() const
239{
240 return m_identifier;
241}
242
243/*!
244 \qmlproperty string QtPurchasing::Product::type
245 This property holds the type of the product in the external market place.
246
247 It can hold one of the following values:
248 \list
249 \li Product.Consumable The product is consumable and can be purchased more than once
250 by the same user, granted that the transaction for the previous purchase has been finalized.
251 \li Product.Unlockable The product can only be purchased once per user. If the application
252 is uninstalled and reinstalled on the device (or installed on a new device by the same user),
253 purchases of unlockable products can be restored using the store's
254 \l{QtPurchasing::Store::restorePurchases()}{restorePurchases()} method.
255 \endlist
256
257 When both the \l identifier and type is set, the product is queried from the external market place,
258 and its other properties are updated asynchronously. At this point, the identifier and type
259 can no longer be changed.
260
261 The following example queries an unlockable product named "myUnlockableProduct" from the external
262 market place.
263 \qml
264 Store {
265 Product {
266 identifier: "myUnlockableProduct"
267 type: Product.Unlockable
268
269 // ...
270 }
271 }
272 \endqml
273*/
274void QInAppProductQmlType::setType(QInAppProductQmlType::ProductType type)
275{
276 if (m_type == type)
277 return;
278
279 if (m_status != Uninitialized) {
280 qWarning(msg: "A product's type cannot be changed once the product has been initialized.");
281 return;
282 }
283
284 m_type = type;
285 if (m_componentComplete)
286 updateProduct();
287
288 emit typeChanged();
289}
290
291QInAppProductQmlType::ProductType QInAppProductQmlType::type() const
292{
293 return m_type;
294}
295
296/*!
297 \qmlproperty enumeration QtPurchasing::Product::status
298 This property holds the current status of the product in the registration sequence.
299
300 \list
301 \li Product.Uninitialized - This is initial status, before the identifier property has been set.
302 \li Product.PendingRegistration - Indicates that the product is currently being queried from the
303 external market place. The product gets this status when its identifier is set.
304 \li Product.Registered - Indicates that the product was successfully found in the external market
305 place. Its price can now be queried and the product can be purchased.
306 \li Product.Unknown - The product could not be found in the external market place. This could
307 for example be due to misspelling the product identifier.
308 \endlist
309
310 \qml
311 Store {
312 Product {
313 identifier: "myConsumableProduct"
314 type: Product.Consumable
315 onStatusChanged: {
316 switch (status) {
317 case Product.PendingRegistration: console.debug("Registering " + identifier); break
318 case Product.Registered: console.debug(identifier + " registered with price " + price); break
319 case Product.Unknown: console.debug(identifier + " was not found in the market place"); break
320 }
321 }
322
323 // ...
324 }
325 }
326 \endqml
327*/
328QInAppProductQmlType::Status QInAppProductQmlType::status() const
329{
330 return m_status;
331}
332
333/*!
334 \qmlproperty string QtPurchasing::Product::price
335 This property holds the price of the product once it has been successfully queried from the
336 external market place. The price is a string consisting of both currency and value, and is
337 usually localized to the current user.
338
339 For example, the following example displays the price of the unlockable product named
340 "myUnlockableProduct":
341 \code
342 Store {
343 Product {
344 id: myUnlockableProduct
345 identifier: "myUnlockableProduct"
346 type: Product.Unlockable
347
348 // ...
349 }
350 }
351
352 Text {
353 text: myUnlockableProduct.status === Product.Registered
354 ? "Price is " + myUnlockableProduct.price
355 : "Price unknown at the moment"
356 }
357 \endcode
358
359 When run in a Norwegian locale, this code could for instance display "Price is kr 6,00" for a one-dollar product.
360*/
361QString QInAppProductQmlType::price() const
362{
363 return m_product != 0 ? m_product->price() : QString();
364}
365
366/*!
367 \qmlproperty string QtPurchasing::Product::title
368 This property holds the title of the product once it has been successfully queried from the
369 external market place. The title is localized if the external market place has defined a title
370 in the current users locale.
371*/
372QString QInAppProductQmlType::title() const
373{
374 return m_product != 0 ? m_product->title() : QString();
375}
376
377/*!
378 \qmlproperty string QtPurchasing::Product::description
379 This property holds the description of the product once it has been successfully queried from the
380 external market place. The title is localized if the external market place has defined a description
381 in the current users locale.
382*/
383QString QInAppProductQmlType::description() const
384{
385 return m_product != 0 ? m_product->description() : QString();
386}
387
388void QInAppProductQmlType::setProduct(QInAppProduct *product)
389{
390 if (m_product == product)
391 return;
392
393 QString oldPrice = price();
394 QString oldTitle = title();
395 QString oldDescription = description();
396 m_product = product;
397 if (price() != oldPrice)
398 emit priceChanged();
399 if (title() != oldTitle)
400 emit titleChanged();
401 if (description() != oldDescription)
402 emit descriptionChanged();
403}
404
405void QInAppProductQmlType::handleProductRegistered(QInAppProduct *product)
406{
407 if (product->identifier() == m_identifier) {
408 Q_ASSERT(product->productType() == QInAppProduct::ProductType(m_type));
409 setProduct(product);
410 if (m_status != Registered) {
411 m_status = Registered;
412 emit statusChanged();
413 }
414 }
415}
416
417void QInAppProductQmlType::handleProductUnknown(QInAppProduct::ProductType, const QString &identifier)
418{
419 if (identifier == m_identifier) {
420 setProduct(0);
421 if (m_status != Unknown) {
422 m_status = Unknown;
423 emit statusChanged();
424 }
425 }
426}
427
428void QInAppProductQmlType::handleTransaction(QInAppTransaction *transaction)
429{
430 if (transaction->product()->identifier() != m_identifier)
431 return;
432
433 if (transaction->status() == QInAppTransaction::PurchaseApproved)
434 emit purchaseSucceeded(transaction);
435 else if (transaction->status() == QInAppTransaction::PurchaseRestored)
436 emit purchaseRestored(transaction);
437 else
438 emit purchaseFailed(transaction);
439}
440
441/*!
442 \qmlmethod QtPurchasing::Product::purchase()
443
444 Launches the purchasing process for this product. The purchasing process is asynchronous.
445 When it completes, either the \l onPurchaseSucceeded or the \l onPurchaseFailed handler
446 in the object will be called with the resulting transaction.
447*/
448void QInAppProductQmlType::purchase()
449{
450 if (m_product != 0 && m_status == Registered)
451 m_product->purchase();
452 else
453 qWarning(msg: "Attempted to purchase unregistered product");
454}
455
456/*!
457 \qmlsignal QtPurchasing::Product::onPurchaseSucceeded(object transaction)
458
459 This handler is called when a product has been purchased successfully. It is triggered
460 when the application has called purchase() on the product and the user has subsequently
461 confirmed the purchase, for example by entering their password.
462
463 All products should have a handler for onPurchaseSucceeded. This handler should in turn
464 save information about the purchased product and when the information has been stored
465 and verified, it should call finalize() on the \a transaction object.
466
467 The handler should support being called multiple times for the same purchase. For example,
468 the application execution might by accident be interrupted after saving the purchase
469 information, but before finalizing the transaction. In this case, the handler should
470 verify that the information is already stored in the persistent storage and then finalize
471 the transaction.
472
473 The following example attempts to store the purchase state of a consumable
474 product using a custom made function. It only finalizes the transaction if saving the
475 data was successful. Otherwise, it calls another custom function to display an error
476 message to the user.
477
478 \qml
479 Store {
480 Product {
481 id: myConsumableProduct
482 identifier: "myConsumableProduct"
483 type: Product.Consumable
484
485 onPurchaseSucceeded: {
486 if (myStorage.savePurchaseInformation(identifier)) {
487 transaction.finalize()
488 } else {
489 myDisplayHelper.message("Failed to store purchase information. Is there available storage?")
490 }
491 }
492
493 // ...
494 }
495 }
496 \endqml
497
498 If the transaction is not finalized, the onPurchaseSucceeded handler will be called again
499 the next time the product is registered (on application startup.) This means that if saving
500 the information failed, the user will have the opportunity of rectifying the problem (for
501 example by deleting something else to make space for the data) and the transaction will
502 be completed once they restart the application and the problem has been solved.
503
504 \note A purchased, consumable product can not be purchased again until its previous transaction
505 is finalized.
506*/
507
508/*!
509 \qmlsignal QtPurchasing::Product::onPurchaseFailed(object transaction)
510
511 This handler is called when a purchase was requested for a given product, but the purchase
512 failed. This will typically happen if the application calls purchase() on a product, and
513 the user subsequently cancels the purchase. It could also happen under other circumstances,
514 for example if there is no suitable network connection.
515
516 All products should have an \c onPurchaseFailed handler.
517
518 After a proper reaction is taken, the finalize() function should be called on the \a transaction
519 object. If this is not done, the handler may be called again the next time the product is registered.
520
521 The following example reacts to a failed purchase attempt by calling a custom function to display a
522 message to the user.
523 \qml
524 Store {
525 Product {
526 id: myConsumableProduct
527 identifier: "myConsumableProduct"
528 type: Product.Consumable
529
530 onPurchaseFailed: {
531 myDisplayHelper.message("Product was not purchased. You have not been charged.")
532 transaction.finalize()
533 }
534
535 // ...
536 }
537 }
538 \endqml
539*/
540
541/*!
542 \qmlsignal QtPurchasing::Product::onPurchaseRestored(object transaction)
543
544 This handler is called when a previously purchased unlockable product is restored. This
545 can happen when the \l{QtPurchasing::Store::restorePurchases()}{restorePurchases()} function
546 in the current \l Store is called.
547 The \c onPurchaseRestored handler will then be called for each unlockable product which
548 has previously been purchased by the user.
549
550 Applications which uses the \l{QtPurchasing::Store::restorePurchases()}{restorePurchases()}
551 function should include this handler
552 in all unlockable products. In the handler, the application should make sure information
553 about the purchase is stored and call \l{QtPurchasing::Transaction::finalize()}{finalize()}
554 on the \a transaction object if
555 the information has been successfully stored (or has been verified to already be stored).
556
557 The following example calls a custom function which either saves the information about
558 the purchase or verifies that it is already saved. When the data has been verified, it
559 finalizes the transaction. If it could not be verified, it calls another custom function
560 to display an error message to the user. If the transaction is not finalized, the handler
561 will be called again for the same transaction the next time the product is registered
562 (on application start-up).
563
564 \qml
565 Store {
566 Product {
567 id: myUnlockableProduct
568 identifier: "myUnlockableProduct"
569 type: Product.Unlockable
570
571 onPurchaseRestored: {
572 if (myStorage.savePurchaseInformation(identifier)) {
573 transaction.finalize()
574 } else {
575 myDisplayHelper.message("Failed to store purchase information. Is there available storage?")
576 }
577 }
578
579 // ...
580 }
581 }
582 \endqml
583*/
584
585QT_END_NAMESPACE
586

source code of qtpurchasing/src/imports/purchasing/qinappproductqmltype.cpp