diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-03-03 17:02:20 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-03-03 17:02:20 +0100 |
commit | 7bf82f8dffebe39b0a557a58d0dd030f080658f7 (patch) | |
tree | 630100cd3a22da53601d887837ba68be7c7d296b /docs | |
parent | af275b9bbfa5967ba7d3ff30c8bb15600f3916c6 (diff) | |
download | docs-7bf82f8dffebe39b0a557a58d0dd030f080658f7.tar.gz docs-7bf82f8dffebe39b0a557a58d0dd030f080658f7.tar.bz2 docs-7bf82f8dffebe39b0a557a58d0dd030f080658f7.zip |
remove outdated merchant store example, obsoleted by tutorial
Diffstat (limited to 'docs')
-rw-r--r-- | docs/example-essay-store.rst | 625 |
1 files changed, 0 insertions, 625 deletions
diff --git a/docs/example-essay-store.rst b/docs/example-essay-store.rst deleted file mode 100644 index c76ce519..00000000 --- a/docs/example-essay-store.rst +++ /dev/null @@ -1,625 +0,0 @@ -.. - This file is part of GNU TALER. - - Copyright (C) 2014, 2015, 2016 INRIA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 2.1, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - - @author Marcello Stanisci - -==================== -Example: Essay Store -==================== - -This section shows how to set up a merchant :ref:`frontend <merchant-arch>`, and is -inspired by our demonstration shop running at `https://shop.demo.taler.net/`. -It is recommended that the reader is already familiar with the -:ref:`payment protocol and terminology <payprot>`. - -The code we are going to describe is available at -https://git.taler.net/merchant-frontends.git/tree/talerfrontends/blog -and is implemented in Python+Flask. - -The desired effect is the homepage showing a list of buyable articles, and once the -user clicks one of them, they will either get the Taler -`contract <https://api.taler.net/api-merchnat.html#contract>`_ -or a credit card paywall if they have no Taler wallet installed. - -Each article thus links to a `offer URL`, whose layout is shown below. - - `https://shop.demo.taler.net/essay/Appendix_A:_A_Note_on_Software` - -Once the server side logic receives a request for a offer URL, it needs to -instruct the wallet to retrieve a Taler contract. This action can be taken -either with or **without** the use of JavaScript, see the next section. - ------------------------ -Triggering the contract ------------------------ - -It is important to note that the contract is not returned simply -as the offer URL's response, but rather the frontend `instructs` -the browser on how to retrieve the contract. That is needed for -the right handling of the cases where the wallet is not installed. - -.. note:: - - The code samples shown below are intentionally "incomplete", as often - one function contains logic for multiple actions. Thus in order to not - mix concepts form different actions under one section, parts of code not - related to the section being documented have been left out. - -**With JavaScript** - -In this case, the objective is to call the function ``taler.offerContractFrom()`` into the user browser, which will then retrieve the -contract. In order to do that, we return a HTML page, whose -template is in ``talerfrontends/blog/templates/purchase.html``, -that imports ``taler-wallet-lib.js``, so that the function -``taler.offerContractFrom()`` can be invoked into the user's -browser. - -The server side handler for a offer URL needs to render ``purchase.html`` by passing -the right parameters to ``taler.offerContractFrom()``. - -The rendering is done by the ``article`` function at ``talerfrontends/blog/blog.py``, -and is done by Flask's ``render_template()``, see below. - -.. sourcecode:: python - - # 'name' is the article name, and is set to the right value - # by Flask - # The 'data' parameter is used to send images along - # the articles, however its use is beyond the scope of - # this survey. - def article(name, data=None): - ... - ... - - return render_template('templates/purchase.html', - article_name=name, - no_contract=1, - contract_url=quote(contract_url), - data_attribute="data-taler-contractoffer=%s" % contract_url) - -After the rendering, (part of) ``purchase.html`` will look like shown below. - -.. sourcecode:: html - - <html> - <head> - <!-- ... --> - <script src="/static/web-common/taler-wallet-lib.js" type="application/javascript"></script> - <script src="/static/purchase.js" type="application/javascript"></script> - <!-- ... --> - <meta name="contract_url" value="https://shop.demo.taler.net/generate-contract?article_name=Appendix_A:_A_Note_on_Software"> - - </head> - <body> - <!-- ... --> - <!-- ... --> - - <div id="ccfakeform" class="fade"> - <p> - Oops, it looks like you don't have a Taler wallet installed. Why don't you enter - all your credit card details before reading the article? <em>You can also - use GNU Taler to complete the purchase at any time.</em> - </p> - <form> - First name<br> <input type="text"></input><br> - Family name<br> <input type="text"></input><br> - Age<br> <input type="text"></input><br> - Nationality<br> <input type="text"></input><br> - Gender<br> <input type="radio" name="gender">Male</input> - CC number<br> <input type="text"></input><br> - <input type="radio" name="gender">Female</input><br> - </form> - - <form method="get" action="/cc-payment/{{ article_name }}"> - <input type="submit"></input> - </form> - </div> - - <div id="talerwait"> - <em>Processing payment with GNU Taler, please wait <span id="action-indicator"></span></em> - </div> - <!-- ... --> - </body> - </html> - - -The script ``purchase.js`` is now in charge of implementing the behaviour we seek. -It needs to register two handlers: one called whenever the wallet is detected in the -browser, the other if the user has no wallet installed. - -That is done with: - -.. sourcecode:: javascript - - taler.onPresent(handleWalletPresent); - taler.onAbsent(handleWalletAbsent); - -Note that the ``taler`` object is exported by ``taler-wallet-lib.js``, and contains all -is needed to communicate with the wallet. - - -``handleWalletAbsent`` doesn't need to do much: it has to only hide the "please wait" -message and uncover the credit card pay form. See below. - -.. sourcecode:: javascript - - function handleWalletAbsent() { - // Hide "please wait" message - document.getElementById("talerwait").style.display = "none"; - // Uncover credit card pay form - document.body.style.display = ""; - } - -On the other hand, ``handleWalletPresent`` needs to firstly hide the credit card -pay form and show the "please wait" message. After that, it needs to fetch the -contract URL from the responsible ``meta`` tag, and finally invoke ``taler.offerContractFrom()`` using it. See below both parts. - -.. sourcecode:: javascript - - function handleWalletPresent() { - // Hide credit card paywall - document.getElementById("ccfakeform").style.display = "none"; - // Show "please wait" message - document.getElementById("talerwait").style.display = ""; - ... - ... - // Fetch contract URL from 'meta' tag. - let contract_url = document.querySelectorAll("[name=contract_url]")[0]; - // If this call is successful, it will obtain the contract, - // hand it to the wallet, so the wallet can eventually - // show it to the user. - taler.offerContractFrom(decodeURIComponent(contract_url.getAttribute("value"))); - ... - } - -.. note:: - - In order to get our code validated by W3C validators, we can't have inline - JavaScript in our pages, we are forced to import any used script instead. - -**Without JavaScript** - -This case is handled by the function ``article`` defined in -``talerfrontends/blog/blog.py``. Its objective is to set the "402 Payment -Required" HTTP status code, and the HTTP header ``X-Taler-Contract-Url`` -to the actual contract's URL for this purchase. - -Upon returning such a response, the wallet will automatically fetch the -contract from the URL indicated by ``X-Taler-Contract-Url``, and show it -to the user. - -Below is shown how the function ``article`` prepares and returns such a -response. - -.. sourcecode:: python - - # 'name' is the article name, and is set to the right value - # by Flask - # The 'data' parameter is used to send images along - # the articles, however its use is beyond the scope of - # this survey. - def article(name, data=None): - ... - ... - - # Create response. - response = make_response(render_template('templates/fallback.html'), 402) - # Set "X-Taler-Contract-Url" header to the contract's URL. - response.headers["X-Taler-Contract-Url"] = contract_url - return response - -The ``make_response`` function is exported by Flask, so it's beyond the scope -of this document to explain it; however, it returns a "response object" having -the "402 Payment Required" as HTTP status code, and the -HTML file ``talerfrontends/blog/templates/fallback.html`` as the body. -``fallback.html`` contains the credit card pay form, so that if the wallet is -not installed, the browser would keep that page shown. - -``contract_url`` is defined in the earlier steps of the same function; however, -in this example it looks like: - - `https://shop.demo.taler.net/essay/generate-contract?article_name=Appendix_A:_A_Note_on_Software`. - -The frontend will also have to provide the contract. That is done -by the handler ``generate_contract``, defined in -``talerfrontends/blog/blog.py``. See below. - -.. sourcecode:: python - - def generate_contract(): - now = int(time.time()) - tid = random.randint(1, 2**50) - article_name = expect_parameter("article_name") - contract = make_contract(article_name=article_name, tid=tid, timestamp=now) - contract_resp = sign_contract(contract) - logger.info("generated contract: %s" % str(contract_resp)) - return jsonify(**contract_resp) - - -Its task is to feed the ``make_contract`` subroutine with all the -values it needs to generate a contract. Those values are: the timestamp -for the contract, the transaction ID, and the article name; respectively, -``now``, ``tid``, and ``article_name``. - -After ``make_contract`` returns, the variable ``contract`` will hold a -`dict` type that complies with a contract -`proposition <https://api.taler.net/api-merchnat.html#proposition>`_ -We then call ``sign_contract`` feeding it with the proposition, so that -it can forward it to the backend and return it signed. Finally we return -the signed proposition, complying with the -`Offer <https://api.taler.net/api-merchant.html#contract>`_ object. - -For simplicity, any article costs the same price, so the frontend -doesn't need to map articles to prices. - -Both ``make_contract`` and ``sign_contract`` are defined in -``talerfrontends/blog/helpers.py``. - -At this point, the user can accept the contract, which triggers the wallet -to visit the fulfillment page. The main logic for a fulfillment page handler -is to (1) return the claimed product, if it has been paid, or (2) instruct the -wallet to send the payment. - ------------------ -Fulfillment logic ------------------ - -The state accounts for a product being paid or not, so the fulfillment handler -will firstly check that: - -.. sourcecode:: python - - # 'name' is the article name, and is set to the right value - # by Flask - # The 'data' parameter is used to send images along - # the articles, however its use is beyond the scope of - # this survey. - def article(name, data=None): - # Get list of payed articles from the state - payed_articles = session.get("payed_articles", []) - - if name in payed_articles: - ... - # The articles has been paid, so return it to the - # customer. - return send_file(get_article_file(article)) - ... - -In case the article has not been paid yet, the fulfillment handler needs -to `reconstruct` the contract, in order to get a precise reference about the -purchase in being served. - -All the information needed to reconstruct the contract is contained in the -fulfillment URL parameters. See below the URL layout: - - `https://shop.demo.taler.net/essay/Appendix_A:_A_Note_on_Software?uuid=<CONTRACT-HASHCODE>×tamp=<TIMESTAMP>tid=<TRANSACTION_ID>` - -The way the contract is reconstructed is exactly the same as it was generated -in the previous steps: we need to call ``make_contract`` to get the original -`proposition <https://api.taler.net/api-merchnat.html#proposition>`_ and then -``sign_contract``. Recall that aside from allowing the backend to add missing -fields to the proposition, ``sign_contract`` returns the contract hashcode also, -that we should compare with the ``uuid`` parameter provided by the wallet. - -In our blog, all the fulfillment logic is implemented in the function ``article``, -defined in ``talerfrontends/blog/blog.py``. It is important to note that this -function is `the same` function that runs the offer URL; in fact, as long as your -URL design allows it, it is not mandatory to split up things. In our example, the -offer URL differs from the fulfillment URL respect to the number (and type) of -parameters, so the ``article`` function can easily decide whether it has to handle -a "offer" or a "fulfillment" case. See below how the function detects the right -case and reconstructs the contract. - -.. sourcecode:: python - - # 'name' is the article name, and is set to the right value - # by Flask - # The 'data' parameter is used to send images along - # the articles, however its use is beyond the scope of - # this survey. - def article(name, data=None): - - ... - hc = request.args.get("uuid") - tid_str = request.args.get("tid") - timestamp_str = request.args.get("timestamp") - if hc is None or tid_str is None or timestamp_str is None: - # Offer URL case. - contract_url = make_url("/generate-contract", ("article_name",name)) - ... # Go on operating the offer URL and return - - # Fulfillment URL case from here on. - try: - tid = int(tid_str) - except ValueError: - raise MalformedParameterError("tid") - try: - timestamp = int(timestamp_str) - except ValueError: - raise MalformedParameterError("timestamp") - - restored_contract = make_contract(article_name=name, tid=tid, timestamp=timestamp) - contract_resp = sign_contract(restored_contract) - - # Return error if uuid mismatch with the hashcode coming from the backend - if contract_resp["h_proposal_data"] != hc: - e = jsonify(error="contract mismatch", was=hc, expected=contract_resp["h_proposal_data"]) - return e, 400 - - # We save the article's name in the state since after - # receiving the payment this value will point to the - # article to be delivered to the customer. Note how the - # contract's hashcode is used to index the state. - session[hc] = si = session.get(hc, {}) - si['article_name'] = name - - -After a successful contract reconstruction, the handler needs to instruct -the wallet to actually send the payment. There are as usual two ways this -can be accomplished: with and without JavaScript. - -**With JavaScript** - -.. - Mention that the template is the same we used for a offer URL! - -We return a HTML page, whose template is in -``talerfrontends/blog/templates/purchase.html``, that imports ``taler-wallet-lib.js``, -so that the function ``taler.executePayment()`` can be invoked into the user's -browser. - -The fulfillment handler needs to render ``purchase.html`` so that the right -parameters get passed to ``taler.executePayment()``. - -See below how the function ``article`` does the rendering. - -.. sourcecode:: python - - # 'name' is the article name, and is set to the right value - # by Flask - # The 'data' parameter is used to send images along - # the articles, however its use is beyond the scope of - # this survey. - def article(name, data=None): - - ... - ... - - return render_template('templates/purchase.html', - hc=hc, - pay_url=quote(pay_url), - offering_url=quote(offering_url), - article_name=name, - no_contract=0, - data_attribute="data-taler-executecontract=%s,%s,%s" % (hc, pay_url, offering_url)) - -After the rendering, (part of) ``purchase.html`` will look like shown below. - -.. sourcecode:: html - - ... - <script src="/static/web-common/taler-wallet-lib.js" type="application/javascript"></script> - <script src="/static/purchase.js" type="application/javascript"></script> - ... - <meta name="pay_url" value="https://shop.demo.taler.net/pay"> - <meta name="offering_url" value="https://shop.demo.taler.net/essay/Appendix_A:_A_Note_on_Software"> - <!-- Fake hashcode --> - <meta name="hc" value="D7D5HDJRP36GTBBRGHXP7204VR773HHQBNFFCY5YY4P18026PAJ0"> - - ... - ... - - <div id="ccfakeform" class="fade"> - <p> - Oops, it looks like you don't have a Taler wallet installed. Why don't you enter - all your credit card details before reading the article? <em>You can also - use GNU Taler to complete the purchase at any time.</em> - </p> - - <form> - <!-- Credit card pay form. --> - </form> - </div> - - <div id="talerwait"> - <em>Processing payment with GNU Taler, please wait <span id="action-indicator"></span></em> - </div> - ... - -The script ``purchase.js`` is now in charge of calling ``taler.executePayment()``. -It will try to register two handlers: one called whenever the wallet is detected in the -browser, the other if the user has no wallet installed. - -That is done with: - -.. sourcecode:: javascript - - taler.onPresent(handleWalletPresent); - taler.onAbsent(handleWalletAbsent); - -.. note:: - - So far, the template and the imported script (``purchase.js``) - are exactly the same as the offer URL case, since we use them - for both cases. See below how the script distinguishes "offer" - from "fulfillment" case. - -Note that the ``taler`` object is exported by ``taler-wallet-lib.js``, and contains all -is needed to communicate with the wallet. - - -``handleWalletAbsent`` doesn't need to do much: it has to only hide the "please wait" -message and uncover the credit card pay form. See below. - -.. sourcecode:: javascript - - function handleWalletAbsent() { - // Hide "please wait" message - document.getElementById("talerwait").style.display = "none"; - // Uncover credit card pay form - document.body.style.display = ""; - } - -On the other hand, ``handleWalletPresent`` needs to firstly hide the credit card -pay form and show the "please wait" message. After that, it needs to fetch the -needed parameters from the responsible ``meta`` tags, and finally invoke -``taler.offerContractFrom()`` using those parameters. See below its whole definition. -Note, that since we are in the fulfillment case, the credit card pay form is `almost` -useless, as it is highly unlikely that the wallet is not installed. - -.. sourcecode:: javascript - - function handleWalletPresent() { - // Hide the credit card pay form - document.getElementById("ccfakeform").style.display = "none"; - // Show "please wait" message - document.getElementById("talerwait").style.display = ""; - - // The `no_contract` value is provided by the function `article` via a - // 'meta' tag in the template. When this value equals 1, then we are in the - // "offer" URL case, otherwise we are in the "fulfillment" URL case. - let no_contract = document.querySelectorAll("[name=no_contract]")[0]; - if (Number(no_contract.getAttribute("value"))) { - // "Offer" case - let contract_url = document.querySelectorAll("[name=contract_url]")[0]; - taler.offerContractFrom(decodeURIComponent(contract_url.getAttribute("value"))); - } - else { - // "Fulfillment" case. - let hc = document.querySelectorAll("[name=hc]")[0]; - let pay_url = document.querySelectorAll("[name=pay_url]")[0]; - let offering_url = document.querySelectorAll("[name=offering_url]")[0]; - taler.executePayment(hc.getAttribute("value"), - decodeURIComponent(pay_url.getAttribute("value")), - decodeURIComponent(offering_url.getAttribute("value"))); - } - } - -Once the browser executes ``taler.executePayment(...)``, the wallet will send the coins -to ``pay_url``. Once the payment succeeds, the wallet will again visit the -fulfillment URL, this time getting the article thanks to the "payed" status set by -the ``pay_url`` handler. - -**Without JavaScript** - -This case is handled by the function ``article`` defined in -``talerfrontends/blog/blog.py``. Its objective is to set the "402 Payment -Required" HTTP status code, along with the HTTP headers ``X-Taler-Contract-Hash``, -``X-Taler-Pay-Url``, and ``X-Taler-Offer-Url``. - -.. - FIXME: - Are those three parameters previously introduced? - -Upon returning such a response, the wallet will automatically send the -payment to the URL indicated in ``X-Taler-Pay-Url``. - -The excerpt below shows how the function ``article`` prepares and returns such a -response. - -.. sourcecode:: python - - # 'name' is the article name, and is set to the right value - # by Flask - # The 'data' parameter is used to send images along - # the articles, however its use is beyond the scope of - # this survey. - def article(name, data=None): - ... - - # 'make_response' is exported by Flask. It returns a - # "response object" with customizable status code, HTTP - # headers and body - response = make_response(render_template('templates/fallback.html'), 402) - response.headers["X-Taler-Contract-Hash"] = hc - response.headers["X-Taler-Pay-Url"] = pay_url - response.headers["X-Taler-Offer-Url"] = offering_url - return response - -The template ``fallback.html`` contains the credit card pay form, which will be -used in the rare case where the wallet would not be detected in a fulfillment -session. Once the payment succeeds, the wallet will again visits the -fulfillment URL, this time getting the article thanks to the "payed" status set by -the ``pay_url`` handler. - ---------- -Pay logic ---------- - -The pay handler for the blog is implemented by the function -``pay`` at ``talerfrontends/blog/blog.py``. Its main duty is to receive the -`deposit permission <https://api.taler.net/api-merchant.html#DepositPermission>`_ -from the wallet, forward it to the backend, and return the outcome -to the wallet. See below the main steps of its implementation. - -.. sourcecode:: python - - def pay(): - # Get the uploaded deposit permission - deposit_permission = request.get_json() - - if deposit_permission is None: - e = jsonify(error="no json in body") - return e, 400 - - # Pick the contract's hashcode from deposit permission - hc = deposit_permission.get("h_proposal_data") - - # Return error if no hashcode was found - if hc is None: - e = jsonify(error="malformed deposit permission", hint="h_proposal_data missing") - return e, 400 - - # Get a handle to the state for this contract, using the - # hashcode from deposit permission as the index - si = session.get(hc) - - # If no session was found for this contract, then either it - # expired or one of the hashcodes (the one we got from - # reconstructing the contract in the fulfillment handler, - # and the one we just picked from the deposit permission) - # is bogus. Note how using the contract's hashcode as index - # makes harder for the wallet to use different hashcodes - # in different steps of the protocol. - if si is None: - e = jsonify(error="no session for contract") - return e, 400 - - # Forward the deposit permission to the backend - r = requests.post(urljoin(BACKEND_URL, 'pay'), json=deposit_permission) - - # Return error if the backend returned a HTTP status code - # other than 200 OK - if 200 != r.status_code: - raise BackendError(r.status_code, r.text) - - # The payment went through.. - ... - - # Resume the article name - article = si["article_name"] - - # Set the article's state as "payed". This is realized by - # appending it to a *list* of articles the customer is currently - # allowed to read. - payed_articles = session["payed_articles"] = session.get("payed_articles", []) - if article not in payed_articles: - payed_articles.append(article) - - ... - - # Return success - return r.text, 200 |