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 | |
35 | QT_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 | |
54 | QInAppProductQmlType::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 | */ |
119 | void 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 | |
140 | QInAppStoreQmlType *QInAppProductQmlType::store() const |
141 | { |
142 | return m_store; |
143 | } |
144 | |
145 | void 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 | */ |
175 | void 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 | |
191 | void 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 | */ |
233 | void QInAppProductQmlType::resetStatus() |
234 | { |
235 | updateProduct(); |
236 | } |
237 | |
238 | QString 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 | */ |
274 | void 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 | |
291 | QInAppProductQmlType::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 | */ |
328 | QInAppProductQmlType::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 | */ |
361 | QString 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 | */ |
372 | QString 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 | */ |
383 | QString QInAppProductQmlType::description() const |
384 | { |
385 | return m_product != 0 ? m_product->description() : QString(); |
386 | } |
387 | |
388 | void 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 | |
405 | void 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 | |
417 | void 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 | |
428 | void 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 | */ |
448 | void 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 | |
585 | QT_END_NAMESPACE |
586 | |