diff options
Diffstat (limited to 'design-documents')
-rw-r--r-- | design-documents/023-taler-kyc.rst | 632 | ||||
-rw-r--r-- | design-documents/046-mumimo-contracts.rst | 2 | ||||
-rw-r--r-- | design-documents/053-wallet-ui.rst | 16 | ||||
-rw-r--r-- | design-documents/054-dynamic-form.rst | 673 |
4 files changed, 608 insertions, 715 deletions
diff --git a/design-documents/023-taler-kyc.rst b/design-documents/023-taler-kyc.rst index c20844b3..786b0ca7 100644 --- a/design-documents/023-taler-kyc.rst +++ b/design-documents/023-taler-kyc.rst @@ -200,12 +200,12 @@ user for *voluntary* KYC processes related to attestation (#7365). Proposed Solution ================= -The main state of an account is represented by a set of `KycRules` (the +The main state of an account is represented by a set of `KYC rules <KycRule>` (the `LegitimizationRuleSet`) which specify the current *rules* to apply to transactions involving the account. Rules can *exposed* to the account owner, or can be secret. Each *rule* specifies certain *conditions* which, if met, -*trigger* a single specific *measure*. After a *rule* was *triggered* and -before the *outcome* of the respective *measure* has been produced (say +*trigger* a set of *measures*. After a *rule* was *triggered* and +before the *outcome* of a respective *measure* has been produced (say because the user did not yet enter their data or the AML officer is still reviewing the case), the existing rules remain in force. Rules have a display priority, and if a second rule with a higher display priority is also @@ -431,48 +431,9 @@ expose merchant public key to SPA for wire transfer if needed for KYC. ^^^^^^^^^^^^ When KYC operations are required, various endpoints may respond with a -``451 Unavailable for Legal Reasons`` status code and a `KycNeededRedirect` +``451 Unavailable for Legal Reasons`` status code and a `LegitimziationNeededResponse` body. - .. ts:def:: KycNeededRedirect - - interface KycNeededRedirect { - - // Numeric `error code <error-codes>` unique to the condition. - // Should always be ``TALER_EC_EXCHANGE_GENERIC_KYC_REQUIRED``. - code: number; - - // Human-readable description of the error, i.e. "missing parameter", - // "commitment violation", ... Should give a human-readable hint - // about the error's nature. Optional, may change without notice! - hint?: string; - - // Hash of the payto:// account URI for which KYC - // is required. - h_payto: PaytoHash; - - // Public key associated with the account. The client must sign - // the initial request for the KYC status using the corresponding - // private key. Will be either a reserve public key or a merchant - // (instance) public key. - // - // Absent if no public key is currently associated - // with the account and the client MUST thus first - // credit the exchange via an inbound wire transfer - // to associate a public key with the debited account. - account_pub?: EddsaPublicKey; - - // Identifies a set of measures that were triggered and that are - // now preventing this operation from proceeding. Gives the - // account holder a starting point for understanding why the - // transaction was blocked and how to lift it. The account holder - // should use the number to check for the account's AML/KYC status - // using the ``/kyc-check/$REQUIREMENT_ROW`` endpoint. - requirement_row: Integer; - - } - - New endpoints ^^^^^^^^^^^^^ @@ -489,7 +450,7 @@ New endpoints The requirement row of the ``/kyc-check/`` endpoint encodes the legitimization measure's serial number. It is returned in - `KycNeededRedirect` responses via the ``requirement_row`` field. + `LegitimizationNeededResponse` responses via the ``requirement_row`` field. Given a valid pair of requirement row and account owner signature, the ``/kyc-check/`` endpoint returns either just the KYC status or redirects the @@ -543,79 +504,6 @@ New endpoints :http:statuscode:`404 Not found`: The requirement row is unknown. - **Details:** - - .. ts:def:: AccountKycStatus - - interface AccountKycStatus { - - // Current AML state for the target account. True if - // operations are not happening due to staff processing - // paperwork *or* due to legal requirements (so the - // client cannot do anything but wait). - // - // Note that not every AML staff action may be legally - // exposed to the client, so this is merely a hint that - // a client should be told that AML staff is currently - // reviewing the account. AML staff *may* review - // accounts without this flag being set! - aml_review: boolean; - - // Access token needed to construct the ``/kyc-spa/`` - // URL that the user should open in a browser to - // proceed with the KYC process (optional if the status - // type is ``200 Ok``, mandatory if the HTTP status - // is ``202 Accepted``). - access_token: AccountAccessToken; - - // Array with limitations that currently apply to this - // account and that may be increased or lifted if the - // KYC check is passed. - // Note that additional limits *may* exist and not be - // communicated to the client. If such limits are - // reached, this *may* be indicated by the account - // going into ``aml_review`` state. However, it is - // also possible that the exchange may legally have - // to deny operations without being allowed to provide - // any justification. - // The limits should be used by the client to - // possibly structure their operations (e.g. withdraw - // what is possible below the limit, ask the user to - // pass KYC checks or withdraw the rest after the time - // limit is passed, warn the user to not withdraw too - // much or even prevent the user from generating a - // request that would cause it to exceed hard limits). - limits?: AccountLimit[]; - - } - - .. ts:def:: AccountLimit - - interface AccountLimit { - - // Operation that is limited. - // Must be one of "WITHDRAW", "DEPOSIT", "P2P-RECEIVE" - // or "WALLET-BALANCE". - operation_type: string; - - // Timeframe during which the limit applies. - timeframe: RelativeTime; - - // Maximum amount allowed during the given timeframe. - // Zero if the operation is simply forbidden. - threshold: Amount; - - // True if this is a soft limit that could be raised - // by passing KYC checks. Clients *may* deliberately - // try to cross limits and trigger measures resulting - // in 451 responses to begin KYC processes. - // Clients that are aware of hard limits *should* - // inform users about the hard limit and prevent flows - // in the UI that would cause violations of hard limits. - soft_limit: boolean; - } - - .. http:get:: /kyc-spa/$ACCESS_TOKEN .. http:get:: /kyc-spa/$FILENAME @@ -656,72 +544,13 @@ New endpoints **Response:** + *Etag*: Will be set to the serial ID of the measure. Used for long-polling (only for 200 OK responses). + :http:statuscode:`200 OK`: The body is a `KycProcessClientInformation`. - - *Etag*: Will be set to the serial ID of the measure. Used for long-polling. - - .. ts:def:: KycProcessClientInformation - - interface KycProcessClientInformation { - - // List of requirements. - requirements?: { name : KycRequirementInformation}; - - // True if the client is expected to eventually satisfy all requirements. - // Default (if missing) is false. - is_and_combinator?: boolean - - // List of available voluntary checks the client could pay for. - // Since **vATTEST**. - voluntary_checks?: { name : KycCheckInformation}; - } - - .. ts:def:: KycRequirementInformation - - interface KycRequirementInformation { - - // Which form should be used? Common values include "INFO" - // (to just show the descriptions but allow no action), - // "LINK" (to enable the user to obtain a link via - // ``/kyc-start/``) or any build-in form name supported - // by the SPA. - form: string; - - // English description of the requirement. - description: string; - - // Map from IETF BCP 47 language tags to localized - // description texts. - description_i18n ?: { [lang_tag: string]: string }; - - // ID of the requirement, useful to construct the - // ``/kyc-upload/$ID`` or ``/kyc-start/$ID`` endpoint URLs. - // Present if and only if "form" is not "INFO". The - // ``$ID`` value may itself contain ``/`` or ``?`` and - // basically encode any URL path (and optional arguments). - id?: string; - - } - - .. ts:def:: KycCheckInformation - - // Since **vATTEST**. - interface KycCheckInformation { - - // English description of the check. - description: string; - - // Map from IETF BCP 47 language tags to localized - // description texts. - description_i18n ?: { [lang_tag: string]: string }; - - } - :http:statuscode:`204 No Content`: There are no open KYC requirements or possible voluntary checks the client might perform. - :http:statuscode:`304 Not Modified`: The KYC requirements did not change. @@ -749,7 +578,7 @@ New endpoints The ``$ID`` is unknown to the exchange. :http:statuscode:`409 Conflict`: The upload conflicts with a previous upload. - :http:statuscode:`413 Content Too Large`: + :http:statuscode:`413 Request Entity Too Large`: The body is too large. .. http:post:: /kyc-start/$ID @@ -771,16 +600,6 @@ New endpoints The KYC process was successfully initiated. The URL is in a `KycProcessStartInformation` object. - **Details:** - - .. ts:def:: KycProcessStartInformation - - interface KycProcessStartInformation { - - // URL to open. - redirect_url: string; - } - :http:statuscode:`404 Not Found`: The ``$ID`` is unknown to the exchange. @@ -913,29 +732,7 @@ New endpoints This response comes with a standard `ErrorDetail` response. :http:statuscode:`451 Unavailable for Legal Reasons`: The wallet must undergo a KYC check. A KYC ID was created. - The response will be a `KycNeededRedirect` object. - - **Details:** - - .. ts:def:: WalletKycRequest - - interface WalletKycRequest { - - // Balance threshold (not necessarily exact balance) - // to be crossed by the wallet that (may) trigger - // additional KYC requirements. - balance: Amount; - - // EdDSA signature of the wallet affirming the - // request, must be of purpose - // ``TALER_SIGNATURE_WALLET_ACCOUNT_SETUP`` - reserve_sig: EddsaSignature; - - // long-term wallet reserve-account - // public key used to create the signature. - reserve_pub: EddsaPublicKey; - } - + The response will be a `LegitimizationNeededResponse` object. .. http:get:: /aml/$OFFICER_PUB/measures @@ -959,86 +756,6 @@ New endpoints Information about possible measures is returned in a `AvailableMeasureSummary` object. - **Details:** - - .. ts:def:: AvailableMeasureSummary - - interface AvailableMeasureSummary { - - // Available original measures that can be - // triggered directly by default rules. - roots: { "$measure_name" : MeasureInformation; }; - - // Available AML programs. - programs: { "$prog_name" : AmlProgramRequirement; }; - - // Available KYC checks. - checks: { "$check_name" : KycCheckInformation; }; - - } - - .. ts:def:: MeasureInformation - - interface MeasureInformation { - - // Name of a KYC check. - check_name: string; - - // Name of an AML program. - prog_name: string; - - // Context for the check. Optional. - context?: Object; - - } - - .. ts:def:: AmlProgramRequirement - - interface AmlProgramRequirement { - - // Description of what the AML program does. - description: string; - - // List of required field names in the context to run this - // AML program. SPA must check that the AML staff is providing - // adequate CONTEXT when defining a measure using this program. - context: string[]; - - // List of required attribute names in the - // input of this AML program. These attributes - // are the minimum that the check must produce - // (it may produce more). - inputs: string[]; - - } - - .. ts:def:: KycCheckInformation - - interface KycCheckInformation { - - // Description of the KYC check. Should be shown - // to the AML staff but will also be shown to the - // client when they initiate the check in the KYC SPA. - description: string; - description_i18n: {}; - - // Names of the fields that the CONTEXT must provide - // as inputs to this check. - // SPA must check that the AML staff is providing - // adequate CONTEXT when defining a measure using - // this check. - requires: string[]; - - // Names of the attributes the check will output. - // SPA must check that the outputs match the - // required inputs when combining a KYC check - // with an AML program into a measure. - outputs: string[]; - - // Name of a root measure taken when this check fails. - fallback: string; - } - .. http:get:: /aml/$OFFICER_PUB/kyc-statistics/$NAME Returns the number of KYC events matching the given event type ``$NAME`` in @@ -1064,13 +781,8 @@ New endpoints **Response:** - .. ts:def:: EventCounter - - interface EventCounter { - // Number of events of the specified type in - // the given range. - counter: Integer; - } + :http:statuscode:`200 OK`: + The responds will be an `EventCounter` message. .. http:get:: /aml/$OFFICER_PUB/decisions @@ -1116,31 +828,6 @@ New endpoints :http:statuscode:`409 Conflict`: The designated AML account is not enabled. - **Details:** - - .. ts:def:: AmlDecisions - - interface AmlDecisions { - - // Array of AML decisions matching the query. - records: AmlDecisions[]; - } - - .. ts:def:: AmlRecord - - interface AmlRecord { - - // Which payto-address is this record about. - // Identifies a GNU Taler wallet or an affected bank account. - h_payto: PaytoHash; - - // Row ID of the record. Used to filter by offset. - rowid: Integer; - - // FIXME: more fields here! - } - - .. http:get:: /aml/$OFFICER_PUB/attributes/$H_PAYTO Obtain attributes obtained as part of AML/KYC processes for a @@ -1176,40 +863,7 @@ New endpoints :http:statuscode:`409 Conflict`: The designated AML account is not enabled. - .. ts:def:: KycAttributes - - interface KycAttributes { - - // Matching KYC attribute history of the account. - details: KycDetail[]; - - } - - .. ts:def:: KycDetail - - // FIXME: bad name? - interface KycDetail { - - // Row ID of the record. Used to filter by offset. - rowid: Integer; - - // Name of the configuration section that specifies the provider - // which was used to collect the attributes. NULL if they were - // just uploaded via a form by the account owner. - provider_section?: string; - - // The collected KYC data. NULL if the attribute data could not - // be decrypted (internal error of the exchange, likely the - // attribute key was changed). - attributes?: Object; - - // Time when the KYC data was collected - collection_time: Timestamp; - - } - - - .. http:post:: /aml/$OFFICER_PUB/decision +.. http:post:: /aml/$OFFICER_PUB/decision Make an AML decision. Triggers the respective action and records the justification. @@ -1231,35 +885,6 @@ New endpoints The designated AML account is not enabled or a more recent decision was already submitted. - **Details:** - - .. ts:def:: AmlDecision - - interface AmlDecision { - - // Human-readable justification for the decision. - justification: string; - - // Which payto-address is the decision about? - // Identifies a GNU Taler wallet or an affected bank account. - h_payto: PaytoHash; - - // What are the new rules? - new_rules: LegitimizationRuleSet; - - // True if the account should remain under investigation by AML staff. - bool keep_investigating; - - // When was the decision made? - decision_time: Timestamp; - - // Signature by the AML officer over a `TALER_AmlDecisionPS`. - // Must have purpose ``TALER_SIGNATURE_MASTER_AML_KEY``. - officer_sig: EddsaSignature; - - } - - Modifications to existing endpoints ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1311,17 +936,6 @@ description of the high-level process for different providers. # Which plugin is responsible for this provider? LOGIC = PLUGIN_NAME - # Optional cost, useful if clients want to voluntarily - # trigger authentication procedures for attestation. - # Since **vATTEST**. - COST = EUR:5 - - # Plus additional logic-specific options, e.g.: - AUTHORIZATION_TOKEN = superdupersecret - - # Other logic-specific internal options (example): - FORM_ID = business_legi_form - # Name of a program to run on the output of the plugin # to convert the result into the desired set of attributes. # The converter must create a log for the system administrator @@ -1334,6 +948,17 @@ description of the high-level process for different providers. # (when run regularly). CONVERTER = taler-exchange-helper-$NAME + # Optional cost, useful if clients want to voluntarily + # trigger authentication procedures for attestation. + # Since **vATTEST**. + COST = EUR:5 + + # Plus additional logic-specific options, e.g.: + AUTHORIZATION_TOKEN = superdupersecret + + # Other logic-specific internal options (example): + FORM_ID = business_legi_form + Configuration of possible KYC/AML checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1364,6 +989,7 @@ providers, one per configuration section: VOLUNTARY = YES/NO # Provider id, present only if type is LINK. + # Refers to a ``kyc-provider-$PROVIDER_ID`` section. PROVIDER_ID = id # Name of the SPA form, if type is FORM @@ -1381,7 +1007,7 @@ providers, one per configuration section: DESCRIPTION_I18N = "{"en":"Upload scan of your passport"}" # ';'-separated list of fields that the CONTEXT must - # provided as inputs to this check. For example, + # provide as inputs to this check. For example, # for a FORM of type CHOICE, this might state # ``choices: string[];``. The type after the ":" # is for now purely for documentation and is @@ -1391,8 +1017,11 @@ providers, one per configuration section: # Description of the outputs provided by the check. # Basically, the check's output is expected to - # provide the following fields as inputs into + # provide the following fields as attribute inputs into # a subsequent AML program. + # Only given for type FORM; INFO never has any outputs, + # and for type LINK we can obtain the same information + # from the CONVERTER via ``--list-outputs``. OUTPUTS = business_name street city country registration # **original** measure to take if the check fails @@ -1422,7 +1051,7 @@ configuration section: [kyc-rule-$RULE_NAME] - # Operation that triggers this legitimization. + # Operation that triggers this rule. # Must be one of WITHDRAW, DEPOSIT, P2P-RECEIVE # or WALLET-BALANCE. OPERATION_TYPE = WITHDRAW @@ -1436,8 +1065,8 @@ configuration section: # (under this set of rules). NEXT_MEASURES = SWISSNESS KYB - # "yes" if all NEXT_MEASURES will eventually need - # to be satisfied, "no" if the user has a choice between + # "YES" if all NEXT_MEASURES will eventually need + # to be satisfied, "NO" if the user has a choice between # them. Not actually enforced by the exchange, but # primarily used to inform the user whether this is # an "and" or "or". YES for "and". @@ -1450,7 +1079,7 @@ configuration section: # Defaults to NO if not set. EXPOSED = YES - # Threshold amount above which the legitimization is + # Threshold amount above which the rule is # triggered. The total must be exceeded in the given # timeframe. THRESHOLD = KUDOS:100 @@ -1460,7 +1089,7 @@ configuration section: # Ignored for WALLET-BALANCE. Can be 'forever'. TIMEFRAME = 30 days - # Enabled (default is NO) + # Set to YES to enable the rule (default is NO) ENABLED = NO @@ -1487,113 +1116,6 @@ AML programs are helper programs that can: This is the default behavior if no command-line switches are provided. -.. ts:def:: AmlProgramInput - - interface AmlProgramInput { - - // JSON object that was provided as - // part of the *measure*. This JSON object is - // provided under "context" in the main JSON object - // input to the AML program. This "context" should - // satify both the REQUIRES clause of the respective - // check and the output of "--requires" from the - // AML program's command-line option. - context?: Object; - - // JSON object that captures the - // output of a ``[kyc-provider-]`` or (HTML) FORM. - // The keys in the JSON object will be the attribute - // names and the values must be strings representing - // the data. In the case of file uploads, the data - // MUST be base64-encoded. - attributes: Object; - - // JSON array with the results of historic - // AML desisions about the account. - aml_history: AmlDecisionDetail[]; - - // JSON array with the results of historic - // KYC data about the account. - kyc_history: KycDetail[]; - - } - -.. ts:def:: AmlOutcome - - interface AmlOutcome { - - // Should the client's account be investigated - // by AML staff? - // Defaults to false. - to_investigate?: boolean; - - // Free-form properties about the account. - // Can be used to store properties such as PEP, - // risk category, type of business, hits on - // sanctions lists, etc. - properties?: AccountProperties; - - // Types of events to add to the KYC events table. - // (for statistics). - events?: string[]; - - // KYC rules to apply. Note that this - // overrides *all* of the default rules - // until the ``expiration_time`` and specifies - // the successor measure to apply after the - // expiration time. - new_rules: LegitimizationRuleSet; - - } - -.. ts:def:: KycRule - - interface KycRule { - - // Type of operation to which the rule applies. - operation_type: string; - - // The measures will be taken if the given - // threshold is crossed over the given timeframe. - threshold: Amount; - - // Over which duration should the ``threshold`` be - // computed. All amounts of the respective - // ``operation_type`` will be added up for this - // duration and the sum compared to the ``threshold``. - timeframe: RelativeTime; - - // Array of names of measures to apply. - // Names listed can be original measures or - // custom measures from the `AmlOutcome`. - // A special measure "verboten" is used if the - // threshold may never be crossed. - measures: string[]; - - // True if the rule (specifically, operation_type, - // threshold, timeframe) and the general nature of - // the measures (verboten or approval required) - // should be exposed to the client. - // Defaults to "false" if not set. - exposed?: boolean; - - // True if all the measures will eventually need to - // be satisfied, false if any of the measures should - // do. Primarily used by the SPA to indicate how - // the measures apply when showing them to the user; - // in the end, AML programs will decide after each - // measure what to do next. - // Default (if missing) is false. - is_and_combinator?: boolean; - - // If multiple rules apply to the same account - // at the same time, the number with the highest - // rule determines which set of measures will - // be activated and thus become visible for the - // user. - display_priority: integer; - } - If the AML program fails (exits with a failure code or does not provide well-formed JSON output) the AML/KYC process continues with the FALLBACK measure. This should @@ -1646,8 +1168,9 @@ Finally, the configuration specifies a set of # just an empty JSON object if there is none. CONTEXT = {"choices":["individual","business"]} - # Program to run on the context and check data to + # Program name to run on the context and check data to # determine the outcome and next measure. + # Refers to a ``[aml-program-$PROG_NAME]`` section name. PROGRAM = taler-aml-program If no ``CHECK_NAME`` is provided at all, the AML ``PROGRAM`` is to be run @@ -1738,6 +1261,8 @@ on GET ``/deposits/`` with the respective legitimization requirement row. ) PARTITION BY HASH (access_token); + COMMENT ON TABLE legitimization_measures + IS 'Rules that have been triggered for the account (FIXME: check this is consistent with usage)'; COMMENT ON COLUMN legitimization_measures.access_token IS 'Used to uniquely identify the account and as a symmetric access control mechanism for the SPA'; COMMENT ON COLUMN legitimization_measures.start_time @@ -1885,86 +1410,13 @@ on GET ``/deposits/`` with the respective legitimization requirement row. The ``jmeasures`` JSON in the ``legitimization_measures`` -table has is of type `LegitimizationMeasures`: - -.. ts:def:: LegitimizationMeasures - - interface LegitimizationMeasures { - - // Array of legitimization measures that - // are to be applied. - measures: MeasureInformation[]; - - // True if the client is expected to eventually satisfy all requirements. - // Default (if missing) is false. - is_and_combinator?: boolean; - } - +table is of type `LegitimizationMeasures`. The ``jnew_rules`` JSON in the ``legitimization_outcomes`` -table has is of type `LegitimizationRuleSet`: - -.. ts:def:: LegitimizationRuleSet - - interface LegitimizationRuleSet { - - // When does this set of rules expire and - // we automatically transition to the successor - // measure? - expiration_time: Timestamp; - - // Name of the measure to apply when the expiration time is - // reached. If not set, we refer to the default - // set of rules (and the default account state). - successor_measure?: string; - - // Legitimization rules that are to be applied - // to this account. - rules: KycRule[]; - - // Custom measures that KYC rules and the - // ``successor_measure`` may refer to. - custom_measures: { "$measure_name" : MeasureInformation; }; - } - - -The ``jproperties`` JSON in the ``legitimization_outcomes`` table has is of -type `AccountProperties`. All fields in this object are optional. The actual -properties collected depend fully on the discretion of the exchange operator; -however, some common fields are standardized and thus described here. - -.. ts:def:: AccountProperties - - interface AccountProperties { - - // True if this is a politically exposed account. - // Rules for classifying accounts as politically - // exposed are country-dependent. - pep?: boolean; - - // True if this is a sanctioned account. - // Rules for classifying accounts as sanctioned - // are country-dependent. - sanctioned?: boolean; - - // True if this is a high-risk account. - // Rules for classifying accounts as at-risk - // are exchange operator-dependent. - high_risk?: boolean; - - // Business domain of the account owner. - // The list of possible business domains is - // operator- or country-dependent. - business_domain?: string; - - // Is the client's account currently frozen? - is_frozen?: boolean; - - // Was the client's account reported to the authorities? - was_reported?: boolean; - - } +table is of type `LegitimizationRuleSet`. +The ``jproperties`` JSON in the ``legitimization_outcomes`` table is of +type `AccountProperties`. KYC forms diff --git a/design-documents/046-mumimo-contracts.rst b/design-documents/046-mumimo-contracts.rst index 8cb35316..7d2197d4 100644 --- a/design-documents/046-mumimo-contracts.rst +++ b/design-documents/046-mumimo-contracts.rst @@ -326,7 +326,7 @@ The contract terms v1 will have the following structure: // Start of the validity period of the token. valid_after: Timestamp; - // Number of tokens to be yelded. + // Number of tokens to be issued. // Defaults to one if the field is not provided. number?: Integer; diff --git a/design-documents/053-wallet-ui.rst b/design-documents/053-wallet-ui.rst index 57f9b1f2..792f58e2 100644 --- a/design-documents/053-wallet-ui.rst +++ b/design-documents/053-wallet-ui.rst @@ -132,11 +132,6 @@ Issues the "add" and "send" buttons on this page. * WebEx (image): Screenshot does not show any pending transactions. -* Android (text): Uses "Exchange:" which is - not user-facing terminology. Should only show the URLs. -* Android (minor): Screenshot shows only a "pending" - badge, which seems redundant given "+10 KUDOS inbound" - (too much information?) * iOS (major): Way too much detail shown, way too much explanation text, too many operations (send money, request payment, spend test money!!!!); @@ -150,7 +145,6 @@ Issues 'tab' entirely! * iOS (text): bad icon for "pending outgoing", the "minus" sign does not give me good associations here -* Android (text): Title should be *Balances* (plural) * Android (minor): No "add" and "Send $CURRENCY" buttons on this page. @@ -288,8 +282,6 @@ Issues payments and debit and POS) and again same icon for invoice and withdraw) is sub-optimal. Should pick unique icons for each type of operation. -* Android (text): Button labels should just be - "Send" and "Receive" (without "funds"). * iOS (text): Iconography (+ / -) is also not expressive and redundant with the colors. * iOS (text): Sign of the operation (+ / -) should @@ -587,9 +579,7 @@ Issues the amount **3** times. Maybe only show the amount on top with the wire transfer details, and then at the bottom show the fee ONCE *if* there is one? -* Android(text): uses "exchange", which is verboten * iOS(text): receiver name is missing, MUST be before IBAN -* Android(text): wire transfer subject is third, should be first * WebEx(screenshot): the screenshot does not show how to select an alternative bank (see: Netzbon). Would be nice to show that. * Android(screenshot): the screenshot does not show how to select an alternative bank (see: Netzbon). Would be nice to show that. @@ -648,8 +638,6 @@ Issues * WebEx(text): not point in showing details if there are no fees. * iOS(text): not point in showing details if there are no fees. -* Android(text): still talks about 'exchange' -* Android(text): has the amount twice, useless without fees * iOS(text): status: Done is unnecessary, if we show this screen, it will always be 'done' (ok, theoretically in the middle of withdrawing, but the wire transfer will be done; but then maybe show 'incoming' but hide the status once really "done"). * unclear: "Delete" vs. 'Delete from history" --- two styles, two translations, gain? @@ -711,8 +699,6 @@ Actions Issues ^^^^^^ -* Android(text) has button label "OK", should probably be "Open" for uniformity. -* Android(text) has "Enter Taler URI", while WebEx has clearer text "enter taler:// URI". * iOS (screenshot): lacks dialog (or screenshot?) entirely, not sure if we need manual URL-entry on mobile though, so could be OK! @@ -825,7 +811,6 @@ Actions Issues ^^^^^^ -* Android(text): details say receipt, but WebEx uses the slightly more accurate "Invoice ID". Could also use "Order #"? * iOS(major): delete button is missing, instead only has "Done" @@ -947,7 +932,6 @@ Actions Issues ^^^^^^ -* Android(text): Webex uses "1 Week" instead of "7 days", let's use "week". * iOS(text): 3 min/ 1h are inconsistent; other wallets have 1 day, 7 days, 30 days. We should be consistent. diff --git a/design-documents/054-dynamic-form.rst b/design-documents/054-dynamic-form.rst index d93b3684..5567fc60 100644 --- a/design-documents/054-dynamic-form.rst +++ b/design-documents/054-dynamic-form.rst @@ -1,10 +1,13 @@ -DD 54: Dynamic Web Form -####################### +DD 54: Dynamic Form +################### Summary ======= -This document outlines the approach for implementing a dynamic web form feature. +This document outlines the design of forms defined in the +backend in a JSON file which will be rendered by a client +for asking information to a person. + Motivation ========== @@ -12,12 +15,16 @@ Motivation Currently, creating a new form for a web app involves coding a new page with HTML, CSS, and JS. Exchange AML requires multiple forms, and different instances may have distinct forms based on jurisdiction. +Being able to define forms in a JSON file that a client software +(not just web SPA) could use to ask the information helps to change +it without requiring a new upgrade of the client app. Requirements ============ -A form consist of a layout and a set of fields. +A form consist of a layout, a set of fields and metadata required to +recognice which form configuration was used to produce the saved value. Layout requirements ------------------- @@ -41,159 +48,609 @@ Fields requirements complex composite structure. +Metadata requirements +--------------------- + +* **identification**: the form configuration instance should have an unique + non reusable id. + +* **label**: the form should have a name recognizable for the user + +* **version**: the same form, with the same id, could be updated. This will + increase the version number. A newer form should support older forms. + Proposed Solutions ================== -Forms are initialized using a flexible structure defined by the -TypeScript interface FormType<T>. This interface comprises properties -such as value (current form data), initial (initial form data for resetting), -readOnly (flag to disable input), onUpdate (callback on form data update), -and computeFormState (function to derive the form state based on current data). +The propose solution defines the structure of a form, the fields and additional +type form-configuration which links a form with a set of fields. + +Form metadata +------------- + +This is the root object of the configuration. +.. code-block:: typescript + + type FormMetadata = { + label: string; + id: string; + version: number; + config: FormConfiguration; + }; + + +Form configuration +------------------ + +Defies a basic structure and the set of fields the form is going to have. + +The ``FormConfiguration`` is an enumerated type which list can be extended in the +future. .. code-block:: typescript - interface FormType<T extends object> { - value: Partial<T>; - initial?: Partial<T>; - readOnly?: boolean; - onUpdate?: (v: Partial<T>) => void; - computeFormState?: (v: Partial<T>) => FormState<T>; + type FormConfiguration = DoubleColumnForm; + + type DoubleColumnForm = { + type: "double-column"; + design: DoubleColumnFormSection[]; } + type DoubleColumnFormSection = { + title: string; + description?: string; + fields: UIFormElementConfig[]; + }; -``T``: is the type of the result object -``value``: is a reference to the current value of the result -``initial``: data for resetting -``readOnly``: when true, fields won't allow input -``onUpdate``: notification of the result update -``computeFormState``: compute a new state of the form based on the current value -Form state have the same shape of ``T`` but every field type is ``FieldUIOptions``. +Form fields +----------- -Fields type can be: - * strings - * numbers - * boolean - * arrays - * object +A form can have two type of element: decorative/informative or input. -The field type ``AmountJson`` and ``AbsoluteTime`` are opaque since field is used as a whole. +An example of a decorative element is a grouping element which make all the fields +inside the group look into the same section. This element will not allow the user +to enter information and won't produce any value in the resulting JSON. -The form can be instanciated using +An example of an input field is a text field which allows the user to enter text. +This element should have an ``id`` which will point into the location in which the +value will be stored in the resulting JSON. Note that two field in the same form +with the same ``id`` will result in undefined behavior. + +The ``UIFormElementConfig`` is an enumerated type with all type of fields supported + +.. code-block:: typescript + + type UIFormElementConfig = + | UIFormElementGroup + | UIFormElementCaption + | UIFormFieldAbsoluteTime + | UIFormFieldAmount + | UIFormFieldArray + | UIFormFieldChoiseHorizontal + | UIFormFieldChoiseStacked + | UIFormFieldFile + | UIFormFieldInteger + | UIFormFieldSelectMultiple + | UIFormFieldSelectOne + | UIFormFieldText + | UIFormFieldTextArea + | UIFormFieldToggle; + + +All form elements should extend from ``UIFieldElementDescription`` which defines a base +configuration to show a field. .. code-block:: typescript - import { FormProvider } from "@gnu-taler/web-util/browser"; + type UIFieldElementDescription = { + /* label if the field, visible for the user */ + label: string; + + /* long text to be shown on user demand */ + tooltip?: string; + + /* short text to be shown close to the field, usually below and dimmer*/ + help?: string; + + /* name of the field, useful for a11y */ + name: string; + /* if the field should be initially hidden */ + hidden?: boolean; + + }; + +That will be enough for a decorative form element (like group element or +a text element) but if it defines an input field then it should extend +from ``UIFormFieldBaseConfig`` which add more information to the previously +defined ``UIFieldElementDescription``. -Then the field component can access all the properties by the ``useField(name)`` hook, -which will return .. code-block:: typescript - interface InputFieldHandler<Type> { - value: Type; - onChange: (s: Type) => void; - state: FieldUIOptions; - isDirty: boolean; - } + type UIFormFieldBaseConfig = UIFieldElementDescription & { + /* example to be shown inside the field */ + placeholder?: string; + /* show a mark as required */ + required?: boolean; -``value``: the current value of the field -``onChange``: a function to call anytime the user want to change the value -``state``: the state of the field (hidden, error, etc..) -``isDirty``: if the user already tried to change the value + /* readonly and dim */ + disabled?: boolean; -A set of common form field exist in ``@gnu-taler/web-util``: + /* conversion id to convert the string into the value type + the id should be known to the ui impl + */ + converterId?: string; - * InputAbsoluteTime - * InputAmount - * InputArray - * InputFile - * InputText - * InputToggle + /* property id of the form */ + id: UIHandlerId; + }; -and should be used inside a ``Form`` context. + /** + * string which defined a json path + * + */ + type UIHandlerId = string -.. code-block:: none - function MyFormComponent():VNode { - return <FormProvider> - <InputAmount name="amount" /> - <InputText name="subject" /> - <button type="submit"> Confirm </button> - </FormProvider> - } +The ``id`` property defines the location in which this information is going +to be saved in the JSON result. Formally formally, it should be a ``dot-selector`` + +.. code-block:: ini + + dot-selector = "." dot-member-name + dot-member-name = name-first *name-char + name-first = ALPHA / "_" + name-char = DIGIT / name-first + + DIGIT = %x30-39 ; 0-9 + ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +All the input fields will create a string value located where the id +points, unless a ``convertedId`` is specified. The ``convertedId`` is a reference +to a converter that the client software implements. For example, an input field +with ``convertedId = "Taler.Amount"`` will transform the value the user +entered into a *AmountString* with the currency in the configuration. + + +Description of supported fields +------------------------------- + +All of this fields defines an UI handler which help the user to input +the value with as handy as possible. The type of the field doesn't define +the type of the value in the resulting JSON, that's defined by the ``converterId``. + +Decorative elements +``````````````````` + +To show some additional text + +.. code-block:: typescript + + type UIFormElementCaption = { type: "caption" } & UIFieldElementDescription; + +To group fields in the UI and maybe show a collapsable handler. + +.. code-block:: typescript + + type UIFormElementGroup = { + type: "group"; + fields: UIFormElementConfig[]; + } & UIFieldElementDescription; + Example --------- +''''''' + +.. code-block:: json + + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Decorative elements", + "fields": [ + { + "type": "caption", + "name": "cap", + "label": "This is a caption" + }, + { + "type": "group", + "name": "group", + "label": "The first name and last name are in a group", + "fields": [ + { + "type": "text", + "name": "firstName", + "id": ".person.name", + "label": "First name" + }, + { + "type": "text", + "name": "lastName", + "id": ".person.lastName", + "label": "Last name" + } + ] + } + ] + } + ] + } + } + +.. image:: ../screenshots/dynamic-forms.decorative-elements.png + :width: 400 + +Time input +`````````` + +This may be rendered as a calendar + +.. code-block:: typescript + + type UIFormFieldAbsoluteTime = { + type: "absoluteTimeText"; + max?: TalerProtocolTimestamp; + min?: TalerProtocolTimestamp; + pattern: string; + } & UIFormFieldBaseConfig; + +.. code-block:: json + + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Time inputs", + "fields": [ + { + "type": "absoluteTime", + "name": "thedate", + "id": ".birthdate", + "converterId": "Taler.AbsoluteTime", + "help": "the day you born", + "pattern":"dd/MM/yyyy", + "label": "Birthdate" + } + ] + } + ] + } + } + +.. image:: ../screenshots/dynamic-forms.time.png + :width: 400 + + +Amount input +```````````` + +Money input. + +.. code-block:: typescript + + type UIFormFieldAmount = { + type: "amount"; + max?: Integer; + min?: Integer; + currency: string; + } & UIFormFieldBaseConfig; + +.. code-block:: json + + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Amount inputs", + "fields": [ + { + "type": "amount", + "name": "thedate", + "id": ".amount", + "converterId": "Taler.Amount", + "help": "how much do you have?", + "currency":"EUR", + "label": "Amount" + } + ] + } + ] + } + } + +.. image:: ../screenshots/dynamic-forms.amount.png + :width: 400 -Consider a form shape represented by the TypeScript type: + +List input +`````````` + +This input allows to enter more than element in the same field, and the +resulting JSON will have a json list. The UI should show the elements +already present in the list, and for that it will use ``labelFieldId``. .. code-block:: typescript - type TheFormType = { - name: string, - age: number, - savings: AmountJson, - nextBirthday: AbsoluteTime, - pets: string[], - addres: { - street: string, - city: string, + type UIFormFieldArray = { + type: "array"; + // id of the field shown when the array is collapsed + labelFieldId: UIHandlerId; + fields: UIFormElementConfig[]; + } & UIFormFieldBaseConfig; + +.. code-block:: json + + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Amount inputs", + "fields": [ + { + "type": "array", + "name": "people", + "id": ".people", + "help": "who is coming to the party?", + "labelFieldId": ".name", + "fields": [{ + "type": "text", + "name": "firstName", + "id": ".name", + "label": "First name" + }, + { + "type": "text", + "name": "lastName", + "id": ".lastName", + "label": "Last name" + }], + } + ] + } + ] + } } + + +.. image:: ../screenshots/dynamic-forms.list.png + :width: 400 + +Choice input +```````````` + +To be used when the user need to choose on predefined values + +.. code-block:: typescript + + interface SelectUiChoice { + label: string; + description?: string; + value: string; } -An example instance of this form could be: +A set of buttons next to each other .. code-block:: typescript - const theFormValue: TheFormType = { - name: "Sebastian", - age: 15, - pets: ["dog","cat"], - address: { - street: "long", - city: "big", + type UIFormFieldChoiseHorizontal = { + type: "choiceHorizontal"; + choices: Array<SelectUiChoice>; + } & UIFormFieldBaseConfig; + + +A set of buttons next on top of each other + +.. code-block:: typescript + + type UIFormFieldChoiseStacked = { + type: "choiceStacked"; + choices: Array<SelectUiChoice>; + } & UIFormFieldBaseConfig; + +A drop down list to select one of the elements + +.. code-block:: typescript + + type UIFormFieldSelectOne = { + type: "selectOne"; + choices: Array<SelectUiChoice>; + } & UIFormFieldBaseConfig; + +A drop down list to select multiple of the element, which +will produce a list of values in the resulting JSON. + +.. code-block:: typescript + + type UIFormFieldSelectMultiple = { + type: "selectMultiple"; + max?: Integer; + min?: Integer; + unique?: boolean; + choices: Array<SelectUiChoice>; + } & UIFormFieldBaseConfig; + + +.. code-block:: json + + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Choice inputs", + "fields": [ + { + "type": "choiceHorizontal", + "name": "food", + "label": "Food", + "id": ".food", + "choices": [ + { + "value": "meat", + "label": "Meat" + }, + { + "value": "sushi", + "label": "Sushi" + }, + { + "value": "taco", + "label": "Taco" + }, + { + "value": "salad", + "label": "Salad" + } + ] + } + ] + } + ] + } + } + +.. image:: ../screenshots/dynamic-forms.choice.png + :width: 400 + +File input +`````````` + +.. code-block:: typescript + + type UIFormFieldFile = { + type: "file"; + maxBytes?: Integer; + minBytes?: Integer; + // comma-separated list of one or more file types + // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers + accept?: string; + } & UIFormFieldBaseConfig; + + +.. code-block:: json + + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "File inputs", + "fields": [ + { + "type": "file", + "name": "photo", + "id": ".photo", + "label": "Photo", + "accept": "*.png" + } + ] + } + ] } } +.. image:: ../screenshots/dynamic-forms.file.png + :width: 400 + +Number input +```````````` + +.. code-block:: typescript + + type UIFormFieldInteger = { + type: "integer"; + max?: Integer; + min?: Integer; + } & UIFormFieldBaseConfig; + + +.. image:: ../screenshots/dynamic-forms.number.png + :width: 400 + +Text input +`````````` + +A simple line of text -For such a form, a valid state can be computed using a function like -``computeFormStateBasedOnFormValues``, returning an object indicating -the state of each field, including properties such as ``hidden``, -``disabled``, and ``required``. - - -.. code-block:: javascript - - function computeFormStateBasedOnFormValues(formValues): { - //returning fixed state as an example - //the return state will be commonly be computed from the values of the form - return { - age: { - hidden: true, - }, - pets: { - disabled: true, - elements: [{ - disabled: false, - }], - }, - address: { - street: { - required: true, - error: "the street name was not found", - }, - city: { - required: true, - }, - }, +.. code-block:: typescript + + type UIFormFieldText = { type: "text" } & UIFormFieldBaseConfig; + +A bigger multi-line of text + +.. code-block:: typescript + + type UIFormFieldTextArea = { type: "textArea" } & UIFormFieldBaseConfig; + + +.. image:: ../screenshots/dynamic-forms.text.png + :width: 400 + +Boolean input +````````````` + +.. code-block:: typescript + + type UIFormFieldToggle = { type: "toggle" } & UIFormFieldBaseConfig; + + +.. code-block:: json + + { + "label": "Example form", + "id": "example", + "version": 1, + "config": { + "type": "double-column", + "design": [ + { + "title": "Boolean inputs", + "fields": [ + { + "type": "toggle", + "name": "the_question", + "id": ".the_question", + "label": "Yes or no?" + } + ] + } + ] } } +.. image:: ../screenshots/dynamic-forms.boolean.png + :width: 400 + +Examples +======== |