Compare commits

..

6 Commits

Author SHA1 Message Date
eddadb6491
go to root on retry 2024-12-13 18:02:09 +03:00
b014cf0dcc
remove all non-incmp statements after HALT 2024-12-13 17:01:01 +03:00
a3d63e8bf5
use next node on profile edit 2024-12-13 09:15:01 +03:00
2c57535a4e
check for back 2024-12-12 15:48:54 +03:00
be9d039ef1
update go-vise dep 2024-12-12 15:47:40 +03:00
a799e19afc
remove check for '0' input 2024-12-12 15:47:07 +03:00
40 changed files with 163 additions and 662 deletions

View File

@ -7,6 +7,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -29,10 +30,10 @@ import (
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
build = "dev"
menuSeparator = ": " build = "dev"
) )
func init() { func init() {
@ -44,14 +45,14 @@ type atRequestParser struct{}
func (arp *atRequestParser) GetSessionId(rq any) (string, error) { func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
rqv, ok := rq.(*http.Request) rqv, ok := rq.(*http.Request)
if !ok { if !ok {
logg.Warnf("got an invalid request", "req", rq) log.Printf("got an invalid request:", rq)
return "", handlers.ErrInvalidRequest return "", handlers.ErrInvalidRequest
} }
// Capture body (if any) for logging // Capture body (if any) for logging
body, err := io.ReadAll(rqv.Body) body, err := io.ReadAll(rqv.Body)
if err != nil { if err != nil {
logg.Warnf("failed to read request body", "err", err) log.Printf("failed to read request body:", err)
return "", fmt.Errorf("failed to read request body: %v", err) return "", fmt.Errorf("failed to read request body: %v", err)
} }
// Reset the body for further reading // Reset the body for further reading
@ -61,13 +62,13 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
bodyLog := map[string]string{"body": string(body)} bodyLog := map[string]string{"body": string(body)}
logBytes, err := json.Marshal(bodyLog) logBytes, err := json.Marshal(bodyLog)
if err != nil { if err != nil {
logg.Warnf("failed to marshal request body", "err", err) log.Printf("failed to marshal request body:", err)
} else { } else {
logg.Debugf("received request", "bytes", logBytes) log.Printf("Received request:", string(logBytes))
} }
if err := rqv.ParseForm(); err != nil { if err := rqv.ParseForm(); err != nil {
logg.Warnf("failed to parse form data", "err", err) log.Printf("failed to parse form data: %v", err)
return "", fmt.Errorf("failed to parse form data: %v", err) return "", fmt.Errorf("failed to parse form data: %v", err)
} }
@ -78,7 +79,7 @@ func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
formattedNumber, err := common.FormatPhoneNumber(phoneNumber) formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
if err != nil { if err != nil {
logg.Warnf("failed to format phone number", "err", err) fmt.Printf("Error: %v\n", err)
return "", fmt.Errorf("failed to format number") return "", fmt.Errorf("failed to format number")
} }
@ -130,10 +131,9 @@ func main() {
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator,
} }
if engineDebug { if engineDebug {

View File

@ -23,7 +23,6 @@ import (
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
menuSeparator = ": "
) )
func init() { func init() {
@ -71,10 +70,9 @@ func main() {
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator,
} }
if engineDebug { if engineDebug {

View File

@ -26,7 +26,6 @@ import (
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
menuSeparator = ": "
) )
func init() { func init() {
@ -59,10 +58,9 @@ func main() {
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator,
} }
if engineDebug { if engineDebug {

View File

@ -18,9 +18,8 @@ import (
) )
var ( var (
logg = logging.NewVanilla() logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration") scriptDir = path.Join("services", "registration")
menuSeparator = ": "
) )
func init() { func init() {
@ -50,11 +49,10 @@ func main() {
pfp := path.Join(scriptDir, "pp.csv") pfp := path.Join(scriptDir, "pp.csv")
cfg := engine.Config{ cfg := engine.Config{
Root: "root", Root: "root",
SessionId: sessionId, SessionId: sessionId,
OutputSize: uint32(size), OutputSize: uint32(size),
FlagCount: uint32(128), FlagCount: uint32(128),
MenuSeparator: menuSeparator,
} }
resourceDir := scriptDir resourceDir := scriptDir

View File

@ -7,83 +7,43 @@ import (
"git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/logging"
) )
// DataType is a subprefix value used in association with vise/db.DATATYPE_USERDATA.
//
// All keys are used only within the context of a single account. Unless otherwise specified, the user context is the session id.
//
// * The first byte is vise/db.DATATYPE_USERDATA
// * The last 2 bytes are the DataTyp value, big-endian.
// * The intermediate bytes are the id of the user context.
//
// All values are strings
type DataTyp uint16 type DataTyp uint16
const ( const (
// API Tracking id to follow status of account creation DATA_ACCOUNT DataTyp = iota
DATA_TRACKING_ID = iota DATA_ACCOUNT_CREATED
// EVM address returned from API on account creation DATA_TRACKING_ID
DATA_PUBLIC_KEY DATA_PUBLIC_KEY
// Currently active PIN used to authenticate ussd state change requests DATA_CUSTODIAL_ID
DATA_ACCOUNT_PIN DATA_ACCOUNT_PIN
// The first name of the user DATA_ACCOUNT_STATUS
DATA_FIRST_NAME DATA_FIRST_NAME
// The last name of the user
DATA_FAMILY_NAME DATA_FAMILY_NAME
// The year-of-birth of the user
DATA_YOB DATA_YOB
// The location of the user
DATA_LOCATION DATA_LOCATION
// The gender of the user
DATA_GENDER DATA_GENDER
// The offerings description of the user
DATA_OFFERINGS DATA_OFFERINGS
// The ethereum address of the recipient of an ongoing send request
DATA_RECIPIENT DATA_RECIPIENT
// The voucher value amount of an ongoing send request
DATA_AMOUNT DATA_AMOUNT
// A general swap field for temporary values
DATA_TEMPORARY_VALUE DATA_TEMPORARY_VALUE
// Currently active voucher symbol of user
DATA_ACTIVE_SYM DATA_ACTIVE_SYM
// Voucher balance of user's currently active voucher
DATA_ACTIVE_BAL DATA_ACTIVE_BAL
// String boolean indicating whether use of PIN is blocked
DATA_BLOCKED_NUMBER DATA_BLOCKED_NUMBER
// Reverse mapping of a user's evm address to a session id.
DATA_PUBLIC_KEY_REVERSE DATA_PUBLIC_KEY_REVERSE
// Decimal count of the currently active voucher
DATA_ACTIVE_DECIMAL DATA_ACTIVE_DECIMAL
// EVM address of the currently active voucher
DATA_ACTIVE_ADDRESS DATA_ACTIVE_ADDRESS
) // Start the sub prefix data at 256 (0x0100)
const (
// List of valid voucher symbols in the user context.
DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota DATA_VOUCHER_SYMBOLS DataTyp = 256 + iota
// List of voucher balances for vouchers valid in the user context.
DATA_VOUCHER_BALANCES DATA_VOUCHER_BALANCES
// List of voucher decimal counts for vouchers valid in the user context.
DATA_VOUCHER_DECIMALS DATA_VOUCHER_DECIMALS
// List of voucher EVM addresses for vouchers valid in the user context.
DATA_VOUCHER_ADDRESSES DATA_VOUCHER_ADDRESSES
// List of senders for valid transactions in the user context. DATA_TX_SENDERS
)
const (
DATA_TX_SENDERS = 512 + iota
// List of recipients for valid transactions in the user context.
DATA_TX_RECIPIENTS DATA_TX_RECIPIENTS
// List of voucher values for valid transactions in the user context.
DATA_TX_VALUES DATA_TX_VALUES
// List of voucher EVM addresses for valid transactions in the user context.
DATA_TX_ADDRESSES DATA_TX_ADDRESSES
// List of valid transaction hashes in the user context.
DATA_TX_HASHES DATA_TX_HASHES
// List of transaction dates for valid transactions in the user context.
DATA_TX_DATES DATA_TX_DATES
// List of voucher symbols for valid transactions in the user context.
DATA_TX_SYMBOLS DATA_TX_SYMBOLS
// List of voucher decimal counts for valid transactions in the user context.
DATA_TX_DECIMALS DATA_TX_DECIMALS
) )

View File

@ -84,18 +84,18 @@ func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string,
// Adjust for 0-based indexing // Adjust for 0-based indexing
i := index - 1 i := index - 1
transactionType := "Received" transactionType := "received"
party := fmt.Sprintf("From: %s", strings.TrimSpace(senders[i])) party := fmt.Sprintf("from: %s", strings.TrimSpace(senders[i]))
if strings.TrimSpace(senders[i]) == publicKey { if strings.TrimSpace(senders[i]) == publicKey {
transactionType = "Sent" transactionType = "sent"
party = fmt.Sprintf("To: %s", strings.TrimSpace(recipients[i])) party = fmt.Sprintf("to: %s", strings.TrimSpace(recipients[i]))
} }
formattedDate := formatDate(strings.TrimSpace(dates[i])) formattedDate := formatDate(strings.TrimSpace(dates[i]))
// Build the full transaction detail // Build the full transaction detail
detail := fmt.Sprintf( detail := fmt.Sprintf(
"%s %s %s\n%s\nContract address: %s\nTxhash: %s\nDate: %s", "%s %s %s\n%s\ncontract address: %s\ntxhash: %s\ndate: %s",
transactionType, transactionType,
strings.TrimSpace(values[i]), strings.TrimSpace(values[i]),
strings.TrimSpace(syms[i]), strings.TrimSpace(syms[i]),

View File

@ -20,7 +20,7 @@ type UserDataStore struct {
func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) { func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ DataTyp) ([]byte, error) {
store.SetPrefix(db.DATATYPE_USERDATA) store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId) store.SetSession(sessionId)
k := ToBytes(typ) k := PackKey(typ, []byte(sessionId))
return store.Get(ctx, k) return store.Get(ctx, k)
} }
@ -29,6 +29,6 @@ func (store *UserDataStore) ReadEntry(ctx context.Context, sessionId string, typ
func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error { func (store *UserDataStore) WriteEntry(ctx context.Context, sessionId string, typ DataTyp, value []byte) error {
store.SetPrefix(db.DATATYPE_USERDATA) store.SetPrefix(db.DATATYPE_USERDATA)
store.SetSession(sessionId) store.SetSession(sessionId)
k := ToBytes(typ) k := PackKey(typ, []byte(sessionId))
return store.Put(ctx, k, value) return store.Put(ctx, k, value)
} }

View File

@ -1,5 +0,0 @@
package debug
var (
DebugCap uint32
)

View File

@ -1,84 +0,0 @@
package debug
import (
"fmt"
"encoding/binary"
"git.grassecon.net/urdt/ussd/common"
"git.defalsify.org/vise.git/db"
)
var (
dbTypStr map[common.DataTyp]string = make(map[common.DataTyp]string)
)
type KeyInfo struct {
SessionId string
Typ uint8
SubTyp common.DataTyp
Label string
Description string
}
func (k KeyInfo) String() string {
v := uint16(k.SubTyp)
s := subTypToString(k.SubTyp)
if s == "" {
v = uint16(k.Typ)
s = typToString(k.Typ)
}
return fmt.Sprintf("Session Id: %s\nTyp: %s (%d)\n", k.SessionId, s, v)
}
func ToKeyInfo(k []byte, sessionId string) (KeyInfo, error) {
o := KeyInfo{}
b := []byte(sessionId)
if len(k) <= len(b) {
return o, fmt.Errorf("storage key missing")
}
o.SessionId = sessionId
o.Typ = uint8(k[0])
k = k[1:]
o.SessionId = string(k[:len(b)])
k = k[len(b):]
if o.Typ == db.DATATYPE_USERDATA {
if len(k) == 0 {
return o, fmt.Errorf("missing subtype key")
}
v := binary.BigEndian.Uint16(k[:2])
o.SubTyp = common.DataTyp(v)
o.Label = subTypToString(o.SubTyp)
k = k[2:]
} else {
o.Label = typToString(o.Typ)
}
if len(k) != 0 {
return o, fmt.Errorf("excess key information")
}
return o, nil
}
func FromKey(k []byte) (KeyInfo, error) {
o := KeyInfo{}
if len(k) < 4 {
return o, fmt.Errorf("insufficient key length")
}
sessionIdBytes := k[1:len(k)-2]
return ToKeyInfo(k, string(sessionIdBytes))
}
func subTypToString(v common.DataTyp) string {
return dbTypStr[v + db.DATATYPE_USERDATA + 1]
}
func typToString(v uint8) string {
return dbTypStr[common.DataTyp(uint16(v))]
}

View File

@ -1,48 +0,0 @@
// +build debugdb
package debug
import (
"git.defalsify.org/vise.git/db"
"git.grassecon.net/urdt/ussd/common"
)
func init() {
DebugCap |= 1
dbTypStr[db.DATATYPE_STATE] = "internal state"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT] = "account"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_CREATED] = "account created"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TRACKING_ID] = "tracking id"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY] = "public key"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_CUSTODIAL_ID] = "custodial id"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_PIN] = "account pin"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACCOUNT_STATUS] = "account status"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FIRST_NAME] = "first name"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_FAMILY_NAME] = "family name"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_YOB] = "year of birth"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_LOCATION] = "location"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_GENDER] = "gender"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_OFFERINGS] = "offerings"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_RECIPIENT] = "recipient"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_AMOUNT] = "amount"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TEMPORARY_VALUE] = "temporary value"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_SYM] = "active sym"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_BAL] = "active bal"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_BLOCKED_NUMBER] = "blocked number"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_PUBLIC_KEY_REVERSE] = "public_key_reverse"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_DECIMAL] = "active decimal"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_ACTIVE_ADDRESS] = "active address"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_SYMBOLS] = "voucher symbols"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_BALANCES] = "voucher balances"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_DECIMALS] = "voucher decimals"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_VOUCHER_ADDRESSES] = "voucher addresses"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SENDERS] = "tx senders"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_RECIPIENTS] = "tx recipients"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_VALUES] = "tx values"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_ADDRESSES] = "tx addresses"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_HASHES] = "tx hashes"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DATES] = "tx dates"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_SYMBOLS] = "tx symbols"
dbTypStr[db.DATATYPE_USERDATA + 1 + common.DATA_TX_DECIMALS] = "tx decimals"
}

View File

@ -1,78 +0,0 @@
package debug
import (
"testing"
"git.grassecon.net/urdt/ussd/common"
"git.defalsify.org/vise.git/db"
)
func TestDebugDbSubKeyInfo(t *testing.T) {
s := "foo"
b := []byte{0x20}
b = append(b, []byte(s)...)
b = append(b, []byte{0x00, 0x02}...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 32 {
t.Fatalf("expected 64, got %d", r.Typ)
}
if r.SubTyp != 2 {
t.Fatalf("expected 2, got %d", r.SubTyp)
}
if DebugCap & 1 > 0 {
if r.Label != "tracking id" {
t.Fatalf("expected 'tracking id', got '%s'", r.Label)
}
}
}
func TestDebugDbKeyInfo(t *testing.T) {
s := "bar"
b := []byte{0x10}
b = append(b, []byte(s)...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 16 {
t.Fatalf("expected 16, got %d", r.Typ)
}
if DebugCap & 1 > 0 {
if r.Label != "internal state" {
t.Fatalf("expected 'internal_state', got '%s'", r.Label)
}
}
}
func TestDebugDbKeyInfoRestore(t *testing.T) {
s := "bar"
b := []byte{db.DATATYPE_USERDATA}
b = append(b, []byte(s)...)
k := common.ToBytes(common.DATA_ACTIVE_SYM)
b = append(b, k...)
r, err := ToKeyInfo(b, s)
if err != nil {
t.Fatal(err)
}
if r.SessionId != s {
t.Fatalf("expected %s, got %s", s, r.SessionId)
}
if r.Typ != 32 {
t.Fatalf("expected 32, got %d", r.Typ)
}
if DebugCap & 1 > 0 {
if r.Label != "active sym" {
t.Fatalf("expected 'active sym', got '%s'", r.Label)
}
}
}

View File

@ -1,79 +0,0 @@
package main
import (
"context"
"crypto/sha1"
"flag"
"fmt"
"os"
"path"
"git.defalsify.org/vise.git/logging"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/common"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
func init() {
initializers.LoadEnvVariables()
}
func main() {
config.LoadConfig()
var dbDir string
var sessionId string
var database string
var engineDebug bool
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.Parse()
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
userStore := common.UserDataStore{store}
h := sha1.New()
h.Write([]byte(sessionId))
address := h.Sum(nil)
addressString := fmt.Sprintf("%x", address)
err = userStore.WriteEntry(ctx, sessionId, common.DATA_PUBLIC_KEY, []byte(addressString))
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = userStore.WriteEntry(ctx, addressString, common.DATA_PUBLIC_KEY_REVERSE, []byte(sessionId))
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
err = store.Close()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
}

View File

@ -1,78 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"path"
"git.grassecon.net/urdt/ussd/config"
"git.grassecon.net/urdt/ussd/initializers"
"git.grassecon.net/urdt/ussd/internal/storage"
"git.grassecon.net/urdt/ussd/debug"
"git.defalsify.org/vise.git/logging"
)
var (
logg = logging.NewVanilla()
scriptDir = path.Join("services", "registration")
)
func init() {
initializers.LoadEnvVariables()
}
func main() {
config.LoadConfig()
var dbDir string
var sessionId string
var database string
var engineDebug bool
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
flag.StringVar(&database, "db", "gdbm", "database to be used")
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
flag.Parse()
ctx := context.Background()
ctx = context.WithValue(ctx, "SessionId", sessionId)
ctx = context.WithValue(ctx, "Database", database)
resourceDir := scriptDir
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
store, err := menuStorageService.GetUserdataDb(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
d, err := store.Dump(ctx, []byte(sessionId))
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
for true {
k, v := d.Next(ctx)
if k == nil {
break
}
o, err := debug.FromKey(k)
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
fmt.Printf("%vValue: %v\n\n", o, v)
}
err = store.Close()
if err != nil {
fmt.Fprintf(os.Stderr, err.Error())
os.Exit(1)
}
}

View File

@ -1,28 +0,0 @@
# Internals
## Version
This document describes component versions:
* `urdt-ussd` `v0.5.0-beta`
* `go-vise` `v0.2.2`
## User profile data
All user profile items are stored under keys matching the user's session id, prefixed with the 8-bit value `git.defalsify.org/vise.git/db.DATATYPE_USERDATA` (32), and followed with a 16-big big-endian value subprefix.
For example, given the sessionId `+254123` and the key `git.grassecon.net/urdt-ussd/common.DATA_PUBLIC_KEY` (2) will be stored under the key:
```
0x322b3235343132330002
prefix sessionid subprefix
32 2b323534313233 0002
```
### Sub-prefixes
All sub-prefixes are defined as constants in the `git.grassecon.net/urdt-ussd/common` package. The constant names have the prefix `DATA_`
Please refer to inline godoc documentation for the `git.grassecon.net/urdt-ussd/common` package for details on each data item.

2
go.mod
View File

@ -3,7 +3,7 @@ module git.grassecon.net/urdt/ussd
go 1.23.0 go 1.23.0
require ( require (
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80 git.defalsify.org/vise.git v0.2.1-0.20241209194740-26fc82a76ede
github.com/alecthomas/assert/v2 v2.2.2 github.com/alecthomas/assert/v2 v2.2.2
github.com/gofrs/uuid v4.4.0+incompatible github.com/gofrs/uuid v4.4.0+incompatible
github.com/grassrootseconomics/eth-custodial v1.3.0-beta github.com/grassrootseconomics/eth-custodial v1.3.0-beta

6
go.sum
View File

@ -1,5 +1,7 @@
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80 h1:GYUVXRUtMpA40T4COeAduoay6CIgXjD5cfDYZOTFIKw= git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a h1:LvGKktk0kUnuRN3nF9r15D8OoV0sFaMmQr52kGq2gtE=
git.defalsify.org/vise.git v0.2.1-0.20241212145627-683015d4df80/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck= git.defalsify.org/vise.git v0.2.1-0.20241122120231-9e9ee5bdfa7a/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
git.defalsify.org/vise.git v0.2.1-0.20241209194740-26fc82a76ede h1:BtooQZJDYEgeRKqv8RSxYSbW5jh0UIFJ6zRMEZZn0sc=
git.defalsify.org/vise.git v0.2.1-0.20241209194740-26fc82a76ede/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=

View File

@ -2,7 +2,6 @@ package handlers
import ( import (
"context" "context"
"strings"
"git.defalsify.org/vise.git/asm" "git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/db"
@ -65,11 +64,7 @@ func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
} }
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) { func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) {
replaceSeparatorFunc := func(input string) string { ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService)
return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator)
}
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -69,19 +69,18 @@ func (fm *FlagManager) GetFlag(label string) (uint32, error) {
} }
type Handlers struct { type Handlers struct {
pe *persist.Persister pe *persist.Persister
st *state.State st *state.State
ca cache.Memory ca cache.Memory
userdataStore common.DataStore userdataStore common.DataStore
adminstore *utils.AdminStore adminstore *utils.AdminStore
flagManager *asm.FlagParser flagManager *asm.FlagParser
accountService remote.AccountServiceInterface accountService remote.AccountServiceInterface
prefixDb storage.PrefixDb prefixDb storage.PrefixDb
profile *models.Profile profile *models.Profile
ReplaceSeparatorFunc func(string) string
} }
func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface, replaceSeparatorFunc func(string) string) (*Handlers, error) { func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *utils.AdminStore, accountService remote.AccountServiceInterface) (*Handlers, error) {
if userdataStore == nil { if userdataStore == nil {
return nil, fmt.Errorf("cannot create handler with nil userdata store") return nil, fmt.Errorf("cannot create handler with nil userdata store")
} }
@ -94,13 +93,12 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
prefixDb := storage.NewSubPrefixDb(userdataStore, prefix) prefixDb := storage.NewSubPrefixDb(userdataStore, prefix)
h := &Handlers{ h := &Handlers{
userdataStore: userDb, userdataStore: userDb,
flagManager: appFlags, flagManager: appFlags,
adminstore: adminstore, adminstore: adminstore,
accountService: accountService, accountService: accountService,
prefixDb: prefixDb, prefixDb: prefixDb,
profile: &models.Profile{Max: 6}, profile: &models.Profile{Max: 6},
ReplaceSeparatorFunc: replaceSeparatorFunc,
} }
return h, nil return h, nil
} }
@ -218,7 +216,7 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte)
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore store := h.userdataStore
_, err = store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) _, err = store.ReadEntry(ctx, sessionId, common.DATA_ACCOUNT_CREATED)
if err != nil { if err != nil {
if db.IsNotFound(err) { if db.IsNotFound(err) {
logg.InfoCtxf(ctx, "Creating an account because it doesn't exist") logg.InfoCtxf(ctx, "Creating an account because it doesn't exist")
@ -734,9 +732,7 @@ func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []by
// Setback sets the flag_back_set flag when the navigation is back // Setback sets the flag_back_set flag when the navigation is back
func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) SetBack(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
//TODO: if h.st.Back() {
//Add check if the navigation is lateral nav instead of checking the input.
if string(input) == "0" {
flag_back_set, _ := h.flagManager.GetFlag("flag_back_set") flag_back_set, _ := h.flagManager.GetFlag("flag_back_set")
res.FlagSet = append(res.FlagSet, flag_back_set) res.FlagSet = append(res.FlagSet, flag_back_set)
} }
@ -1685,9 +1681,7 @@ func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte)
return res, err return res, err
} }
formattedData := h.ReplaceSeparatorFunc(string(voucherData)) res.Content = string(voucherData)
res.Content = string(formattedData)
return res, nil return res, nil
} }
@ -1850,14 +1844,13 @@ func (h *Handlers) CheckTransactions(ctx context.Context, sym string, input []by
return res, nil return res, nil
} }
// GetTransactionsList reads the list of transactions from the db and formats them // GetTransactionsList fetches the list of transactions and formats them
func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []byte) (resource.Result, error) { func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string) sessionId, ok := ctx.Value("SessionId").(string)
if !ok { if !ok {
return res, fmt.Errorf("missing session") return res, fmt.Errorf("missing session")
} }
store := h.userdataStore store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY) publicKey, err := store.ReadEntry(ctx, sessionId, common.DATA_PUBLIC_KEY)
if err != nil { if err != nil {
@ -1900,14 +1893,12 @@ func (h *Handlers) GetTransactionsList(ctx context.Context, sym string, input []
value := strings.TrimSpace(values[i]) value := strings.TrimSpace(values[i])
date := strings.Split(strings.TrimSpace(dates[i]), " ")[0] date := strings.Split(strings.TrimSpace(dates[i]), " ")[0]
status := "Received" status := "received"
if sender == string(publicKey) { if sender == string(publicKey) {
status = "Sent" status = "sent"
} }
// Use the ReplaceSeparator function for the menu separator formattedTransactions = append(formattedTransactions, fmt.Sprintf("%d:%s %s %s %s", i+1, status, value, sym, date))
transactionLine := fmt.Sprintf("%d%s%s %s %s %s", i+1, h.ReplaceSeparatorFunc(":"), status, value, sym, date)
formattedTransactions = append(formattedTransactions, transactionLine)
} }
res.Content = strings.Join(formattedTransactions, "\n") res.Content = strings.Join(formattedTransactions, "\n")

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"log" "log"
"path" "path"
"strings"
"testing" "testing"
"git.defalsify.org/vise.git/lang" "git.defalsify.org/vise.git/lang"
@ -33,11 +32,6 @@ var (
flagsPath = path.Join(baseDir, "services", "registration", "pp.csv") flagsPath = path.Join(baseDir, "services", "registration", "pp.csv")
) )
// mockReplaceSeparator function
var mockReplaceSeparator = func(input string) string {
return strings.ReplaceAll(input, ":", ": ")
}
// InitializeTestStore sets up and returns an in-memory database and store. // InitializeTestStore sets up and returns an in-memory database and store.
func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) { func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore) {
ctx := context.Background() ctx := context.Background()
@ -73,15 +67,12 @@ func TestNewHandlers(t *testing.T) {
_, store := InitializeTestStore(t) _, store := InitializeTestStore(t)
fm, err := NewFlagManager(flagsPath) fm, err := NewFlagManager(flagsPath)
if err != nil {
log.Fatal(err)
}
accountService := testservice.TestAccountService{} accountService := testservice.TestAccountService{}
if err != nil {
// Test case for valid UserDataStore t.Logf(err.Error())
}
t.Run("Valid UserDataStore", func(t *testing.T) { t.Run("Valid UserDataStore", func(t *testing.T) {
handlers, err := NewHandlers(fm.parser, store, nil, &accountService, mockReplaceSeparator) handlers, err := NewHandlers(fm.parser, store, nil, &accountService)
if err != nil { if err != nil {
t.Fatalf("expected no error, got %v", err) t.Fatalf("expected no error, got %v", err)
} }
@ -91,30 +82,19 @@ func TestNewHandlers(t *testing.T) {
if handlers.userdataStore == nil { if handlers.userdataStore == nil {
t.Fatal("expected userdataStore to be set in handlers") t.Fatal("expected userdataStore to be set in handlers")
} }
if handlers.ReplaceSeparatorFunc == nil {
t.Fatal("expected ReplaceSeparatorFunc to be set in handlers")
}
// Test ReplaceSeparatorFunc functionality
input := "1:Menu item"
expectedOutput := "1: Menu item"
if handlers.ReplaceSeparatorFunc(input) != expectedOutput {
t.Fatalf("ReplaceSeparatorFunc function did not return expected output: got %v, want %v", handlers.ReplaceSeparatorFunc(input), expectedOutput)
}
}) })
// Test case for nil UserDataStore // Test case for nil userdataStore
t.Run("Nil UserDataStore", func(t *testing.T) { t.Run("Nil UserDataStore", func(t *testing.T) {
handlers, err := NewHandlers(fm.parser, nil, nil, &accountService, mockReplaceSeparator) handlers, err := NewHandlers(fm.parser, nil, nil, &accountService)
if err == nil { if err == nil {
t.Fatal("expected an error, got none") t.Fatal("expected an error, got none")
} }
if handlers != nil { if handlers != nil {
t.Fatal("expected handlers to be nil") t.Fatal("expected handlers to be nil")
} }
expectedError := "cannot create handler with nil userdata store" if err.Error() != "cannot create handler with nil userdata store" {
if err.Error() != expectedError { t.Fatalf("expected specific error, got %v", err)
t.Fatalf("expected error '%s', got '%v'", expectedError, err)
} }
}) })
} }
@ -2002,31 +1982,26 @@ func TestCheckVouchers(t *testing.T) {
func TestGetVoucherList(t *testing.T) { func TestGetVoucherList(t *testing.T) {
sessionId := "session123" sessionId := "session123"
ctx := context.WithValue(context.Background(), "SessionId", sessionId) ctx := context.WithValue(context.Background(), "SessionId", sessionId)
spdb := InitializeTestSubPrefixDb(t, ctx) spdb := InitializeTestSubPrefixDb(t, ctx)
// Initialize Handlers
h := &Handlers{ h := &Handlers{
prefixDb: spdb, prefixDb: spdb,
ReplaceSeparatorFunc: mockReplaceSeparator,
} }
mockSyms := []byte("1:SRF\n2:MILO") expectedSym := []byte("1:SRF\n2:MILO")
// Put voucher sym data from the store // Put voucher sym data from the store
err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), mockSyms) err := spdb.Put(ctx, common.ToBytes(common.DATA_VOUCHER_SYMBOLS), expectedSym)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
expectedSyms := []byte("1: SRF\n2: MILO")
res, err := h.GetVoucherList(ctx, "", []byte("")) res, err := h.GetVoucherList(ctx, "", []byte(""))
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, res.Content, string(expectedSyms)) assert.Equal(t, res.Content, string(expectedSym))
} }
func TestViewVoucher(t *testing.T) { func TestViewVoucher(t *testing.T) {

View File

@ -115,8 +115,7 @@ func(tdb *ThreadGdbmDb) Close() error {
return err return err
} }
func(tdb *ThreadGdbmDb) Dump(ctx context.Context, key []byte) (*db.Dumper, error) { func(tdb *ThreadGdbmDb) Dump(_ context.Context, _ []byte) (*db.Dumper, error) {
tdb.reserve() logg.Warnf("method not implemented for thread gdbm db")
defer tdb.release() return nil, nil
return tdb.db.Dump(ctx, key)
} }

View File

@ -11,7 +11,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n0:Yes\n1:No" "expectedContent": "Do you agree to terms and conditions?\n0:Yes\n1:No"
}, },
{ {
"input": "0", "input": "0",
@ -44,7 +44,7 @@
}, },
{ {
"input": "0", "input": "0",
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n0:Yes\n1:No" "expectedContent": "Do you agree to terms and conditions?\n0:Yes\n1:No"
}, },
{ {
"input": "1", "input": "1",

View File

@ -1,18 +1,7 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_familyname flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
MAP get_current_profile_info MAP get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
RELOAD set_back INCMP _ 0
CATCH _ flag_back_set 1 INCMP edit_family_name_next *
LOAD save_familyname 64
RELOAD save_familyname
CATCH pin_entry flag_familyname_set 1
CATCH select_gender flag_gender_set 0
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_familyname_set 0
INCMP select_gender *

View File

@ -0,0 +1,11 @@
LOAD save_familyname 32
RELOAD save_familyname
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_familyname flag_allow_update 1
RELOAD set_back
CATCH _ flag_back_set 1
CATCH pin_entry flag_familyname_set 1
CATCH select_gender flag_gender_set 0
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH pin_entry flag_familyname_set 0

View File

@ -1,18 +1,7 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_firstname flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
MAP get_current_profile_info MAP get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
RELOAD set_back INCMP _ 0
CATCH _ flag_back_set 1 INCMP edit_first_name_next *
LOAD save_firstname 128
RELOAD save_firstname
CATCH pin_entry flag_firstname_set 1
CATCH edit_family_name flag_familyname_set 0
CATCH edit_gender flag_gender_set 0
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_firstname_set 0

View File

@ -0,0 +1,12 @@
LOAD save_firstname 64
RELOAD save_firstname
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_firstname flag_allow_update 1
RELOAD set_back
CATCH _ flag_back_set 1
CATCH pin_entry flag_firstname_set 1
CATCH edit_family_name flag_familyname_set 0
CATCH select_gender flag_gender_set 0
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH pin_entry flag_firstname_set 0

View File

@ -1,15 +1,7 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_location flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
LOAD save_location 16
MOUT back 0 MOUT back 0
HALT HALT
RELOAD set_back
CATCH _ flag_back_set 1
RELOAD save_location
INCMP _ 0 INCMP _ 0
CATCH pin_entry flag_location_set 1 INCMP edit_location_next *
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_location_set 0
INCMP edit_offerings *

View File

@ -0,0 +1,8 @@
LOAD save_location 16
RELOAD save_location
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_location flag_allow_update 1
RELOAD set_back
CATCH _ flag_back_set 1
CATCH pin_entry flag_location_set 1
CATCH edit_offerings flag_offerings_set 0

View File

@ -1,14 +1,7 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_offerings flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
LOAD save_offerings 8
MOUT back 0 MOUT back 0
HALT HALT
RELOAD set_back
CATCH _ flag_back_set 1
RELOAD save_offerings
INCMP _ 0 INCMP _ 0
CATCH pin_entry flag_offerings_set 1 INCMP edit_offerings_next *
CATCH pin_entry flag_offerings_set 0
INCMP update_profile_items *

View File

@ -0,0 +1,8 @@
LOAD save_offerings 8
RELOAD save_offerings
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_offerings flag_allow_update 1
RELOAD set_back
CATCH _ flag_back_set 1
CATCH pin_entry flag_offerings_set 1
MOVE update_profile_items

View File

@ -1,19 +1,7 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_yob flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
MAP get_current_profile_info MAP get_current_profile_info
MOUT back 0 MOUT back 0
HALT HALT
RELOAD set_back INCMP _ 0
CATCH _ flag_back_set 1 INCMP edit_yob_next *
LOAD verify_yob 6
RELOAD verify_yob
CATCH incorrect_date_format flag_incorrect_date_format 1
LOAD save_yob 32
RELOAD save_yob
CATCH pin_entry flag_yob_set 1
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_yob_set 0
INCMP edit_location *

View File

@ -0,0 +1,13 @@
LOAD save_yob 16
LOAD verify_yob 8
RELOAD verify_yob
RELOAD save_yob
CATCH incorrect_date_format flag_incorrect_date_format 1
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_yob flag_allow_update 1
RELOAD set_back
CATCH _ flag_back_set 1
CATCH pin_entry flag_yob_set 1
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_yob_set 0

View File

@ -2,5 +2,5 @@ LOAD reset_incorrect_date_format 8
MOUT retry 1 MOUT retry 1
MOUT quit 9 MOUT quit 9
HALT HALT
INCMP _ 1 INCMP ^ 1
INCMP quit 9 INCMP quit 9

View File

@ -1,5 +1,3 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH profile_update_success flag_allow_update 1
LOAD get_current_profile_info 0 LOAD get_current_profile_info 0
RELOAD get_current_profile_info RELOAD get_current_profile_info
MOUT male 1 MOUT male 1

View File

@ -1,10 +1,3 @@
LOAD save_gender 32 LOAD save_gender 16
RELOAD save_gender RELOAD save_gender
CATCH incorrect_pin flag_incorrect_pin 1 MOVE set_gender_next
CATCH update_gender flag_allow_update 1
CATCH pin_entry flag_gender_set 1
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_gender_set 0
MOVE edit_yob

View File

@ -0,0 +1 @@
Select gender

View File

@ -0,0 +1,6 @@
CATCH incorrect_pin flag_incorrect_pin 1
CATCH update_gender flag_allow_update 1
CATCH pin_entry flag_gender_set 1
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0

View File

@ -1,10 +1,3 @@
LOAD save_gender 16 LOAD save_gender 16
RELOAD save_gender RELOAD save_gender
CATCH incorrect_pin flag_incorrect_pin 1 MOVE set_gender_next
CATCH update_gender flag_allow_update 1
CATCH pin_entry flag_gender_set 1
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_gender_set 0
MOVE edit_yob

View File

@ -1,10 +1,3 @@
LOAD save_gender 8 LOAD save_gender 16
RELOAD save_gender RELOAD save_gender
CATCH incorrect_pin flag_incorrect_pin 1 MOVE set_gender_next
CATCH update_gender flag_allow_update 1
CATCH pin_entry flag_gender_set 1
CATCH edit_yob flag_yob_set 0
CATCH edit_location flag_location_set 0
CATCH edit_offerings flag_offerings_set 0
CATCH pin_entry flag_gender_set 0
MOVE edit_yob

View File

@ -1,2 +1 @@
Do you agree to terms and conditions? Do you agree to terms and conditions?
https://grassecon.org/pages/terms-and-conditions

View File

@ -1,2 +1 @@
Kwa kutumia hii huduma umekubali sheria na masharti? Kwa kutumia hii huduma umekubali sheria na masharti?
https://grassecon.org/pages/terms-and-conditions