License Key Registration with ECDSA JWT Tokens

* New command-line tool: doodle-admin for signing license keys for
  users. Includes functions to initialize a keypair, sign license keys
  and validate existing keys.
* The Main Menu screen shows a blue "Register Game" button in the bottom
  right corner of the screen, for unregistered users only.
* In Edit Mode, there is a "Help -> Register" menu item that opens the
  License Window.
* The License UI Window lets the user select the license.key file to
  register the game with. If registered, a copy of the key is placed in
  Doodle's profile directory and the licensee name/email is shown in the
  License UI window.
* Unregistered games will show the word "(shareware)" next to the title
  screen version number and Edit Mode status bar.
* No restrictions are yet placed on free versions of the game.
loading-screen
Noah 2021-06-16 21:55:45 -07:00
parent d6f86487f5
commit 0449737607
15 changed files with 833 additions and 30 deletions

View File

@ -9,8 +9,8 @@ import (
"time"
"git.kirsle.net/apps/doodle/cmd/doodad/commands"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/license"
"github.com/urfave/cli/v2"
)
@ -32,7 +32,7 @@ func main() {
app.Usage = "command line interface for Doodle"
var freeLabel string
if balance.FreeVersion {
if !license.IsRegistered() {
freeLabel = " (shareware)"
}

View File

@ -0,0 +1,44 @@
package command
import (
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/urfave/cli/v2"
)
// Key a license key for Sketchy Maze.
var Key *cli.Command
func init() {
Key = &cli.Command{
Name: "key",
Usage: "generate an admin ECDSA signing key",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "public",
Usage: "Filename to write the public key to (.pem)",
Required: true,
},
&cli.StringFlag{
Name: "private",
Usage: "Filename to write the private key to (.pem)",
Required: true,
},
},
Action: func(c *cli.Context) error {
key, err := license.AdminGenerateKeys()
if err != nil {
return cli.Exit(err.Error(), 1)
}
err = license.AdminWriteKeys(key, c.String("private"), c.String("public"))
if err != nil {
return cli.Exit(err.Error(), 1)
}
log.Info("Written private key: %s", c.String("private"))
log.Info("Written public key: %s", c.String("public"))
return nil
},
}
}

View File

@ -0,0 +1,73 @@
package command
import (
"fmt"
"io/ioutil"
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/urfave/cli/v2"
)
// Sign a license key for Sketchy Maze.
var Sign *cli.Command
func init() {
Sign = &cli.Command{
Name: "sign",
Usage: "sign a license key for the paid version of Sketchy Maze.",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "key",
Aliases: []string{"k"},
Usage: "Private key .pem file for signing",
Required: true,
},
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Usage: "User name for certificate",
Required: true,
},
&cli.StringFlag{
Name: "email",
Aliases: []string{"e"},
Usage: "User email address",
Required: true,
},
&cli.StringFlag{
Name: "output",
Aliases: []string{"o"},
Usage: "Output file, default outputs to console",
},
},
Action: func(c *cli.Context) error {
key, err := license.AdminLoadPrivateKey(c.String("key"))
if err != nil {
return cli.Exit(err.Error(), 1)
}
reg := license.Registration{
Name: c.String("name"),
Email: c.String("email"),
}
result, err := license.AdminSignRegistration(key, reg)
if err != nil {
return cli.Exit(err.Error(), 1)
}
// Writing to an output file?
if output := c.String("output"); output != "" {
log.Info("Write to: %s", output)
if err := ioutil.WriteFile(output, []byte(result), 0644); err != nil {
return cli.Exit(err, 1)
}
} else {
fmt.Println(result)
}
return nil
},
}
}

View File

@ -0,0 +1,59 @@
package command
import (
"io/ioutil"
"time"
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log"
"github.com/urfave/cli/v2"
)
// Verify a license key for Sketchy Maze.
var Verify *cli.Command
func init() {
Verify = &cli.Command{
Name: "verify",
Usage: "check the signature on a license key",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "key",
Aliases: []string{"k"},
Usage: "Public key .pem file that signed the JWT",
Required: true,
},
&cli.StringFlag{
Name: "filename",
Aliases: []string{"f"},
Usage: "File name of the license file to validate",
Required: true,
},
},
Action: func(c *cli.Context) error {
key, err := license.AdminLoadPublicKey(c.String("key"))
if err != nil {
return cli.Exit(err.Error(), 1)
}
jwt, err := ioutil.ReadFile(c.String("filename"))
if err != nil {
return cli.Exit(err.Error(), 1)
}
reg, err := license.Validate(key, string(jwt))
if err != nil {
return cli.Exit(err.Error(), 1)
}
log.Info("Registration valid")
log.Info(" Name: %s", reg.Name)
log.Info(" Email: %s", reg.Email)
log.Info(" Issued: %s", time.Unix(reg.IssuedAt, 0))
log.Info(" NBF: %s", time.Unix(reg.NotBefore, 0))
log.Info("Raw:\n%+v", reg)
return nil
},
}
}

59
cmd/doodle-admin/main.go Normal file
View File

@ -0,0 +1,59 @@
// doodle-admin performs secret admin tasks like generating license keys.
package main
import (
"fmt"
"log"
"os"
"sort"
"time"
"git.kirsle.net/apps/doodle/cmd/doodle-admin/command"
"git.kirsle.net/apps/doodle/pkg/branding"
"github.com/urfave/cli/v2"
)
// Build variables.
var (
Build = "N/A"
BuildDate string
)
func init() {
if BuildDate == "" {
BuildDate = time.Now().Format(time.RFC3339)
}
}
func main() {
app := cli.NewApp()
app.Name = "doodle-admin"
app.Usage = "Admin tasks for Sketchy Maze."
app.Version = fmt.Sprintf("%s build %s. Built on %s",
branding.Version,
Build,
BuildDate,
)
app.Flags = []cli.Flag{
&cli.BoolFlag{
Name: "debug, d",
Usage: "enable debug level logging",
},
}
app.Commands = []*cli.Command{
command.Key,
command.Sign,
command.Verify,
}
sort.Sort(cli.FlagsByName(app.Flags))
sort.Sort(cli.CommandsByName(app.Commands))
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
}

View File

@ -15,6 +15,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/bindata"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/apps/doodle/pkg/sound"
@ -51,7 +52,7 @@ func main() {
app.Usage = fmt.Sprintf("%s - %s", branding.AppName, branding.Summary)
var freeLabel string
if balance.FreeVersion {
if !license.IsRegistered() {
freeLabel = " (shareware)"
}

View File

@ -127,7 +127,9 @@ func init() {
ButtonDanger.HoverForeground = ButtonPrimary.Foreground
ButtonBabyBlue.Background = render.RGBA(0, 153, 255, 255)
ButtonBabyBlue.Foreground = render.White
ButtonBabyBlue.HoverBackground = render.RGBA(0, 220, 255, 255)
ButtonBabyBlue.HoverForeground = render.White
ButtonPink.Background = render.RGBA(255, 153, 255, 255)
ButtonPink.HoverBackground = render.RGBA(255, 220, 255, 255)

View File

@ -4,7 +4,7 @@ package branding
const (
AppName = "Sketchy Maze"
Summary = "A drawing-based maze game"
Version = "0.6.1-alpha"
Version = "0.6.1"
Website = "https://www.sketchymaze.com"
Copyright = "2021 Noah Petherbridge"

View File

@ -233,11 +233,6 @@ func (d *Doodle) NewMap() {
// NewDoodad loads a new Doodad in Edit Mode.
func (d *Doodle) NewDoodad(size int) {
if balance.FreeVersion {
d.Flash("Doodad editor is not available in your version of the game.")
return
}
log.Info("Starting a new doodad")
scene := &EditorScene{
DrawingType: enum.DoodadDrawing,
@ -262,9 +257,6 @@ func (d *Doodle) EditDrawing(filename string) error {
log.Info("is a Level type")
scene.DrawingType = enum.LevelDrawing
case ".doodad":
if balance.FreeVersion {
return fmt.Errorf("Doodad editor not supported in your version of the game")
}
scene.DrawingType = enum.DoodadDrawing
default:
return fmt.Errorf("file extension '%s' doesn't indicate its drawing type", ext)

View File

@ -10,6 +10,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/apps/doodle/pkg/enum"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/uix"
@ -51,6 +52,7 @@ type EditorUI struct {
layersWindow *ui.Window
publishWindow *ui.Window
filesystemWindow *ui.Window
licenseWindow *ui.Window
// Palette window.
Palette *ui.Window
@ -508,24 +510,22 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
d.GotoNewMenu()
})
})
if !balance.FreeVersion {
fileMenu.AddItem("New doodad", func() {
u.Scene.ConfirmUnload(func() {
d.Prompt("Doodad size [100]>", func(answer string) {
size := balance.DoodadSize
if answer != "" {
i, err := strconv.Atoi(answer)
if err != nil {
d.Flash("Error: Doodad size must be a number.")
return
}
size = i
fileMenu.AddItem("New doodad", func() {
u.Scene.ConfirmUnload(func() {
d.Prompt("Doodad size [100]>", func(answer string) {
size := balance.DoodadSize
if answer != "" {
i, err := strconv.Atoi(answer)
if err != nil {
d.Flash("Error: Doodad size must be a number.")
return
}
d.NewDoodad(size)
})
size = i
}
d.NewDoodad(size)
})
})
}
})
fileMenu.AddItemAccel("Save", "Ctrl-S*", func() {
if u.Scene.filename != "" {
saveFunc(u.Scene.filename)
@ -688,6 +688,27 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
helpMenu.AddItemAccel("User Manual", "F1", func() {
native.OpenLocalURL(balance.GuidebookPath)
})
helpMenu.AddItem("Register", func() {
if u.licenseWindow == nil {
cfg := windows.License{
Supervisor: u.Supervisor,
Engine: d.Engine,
OnCancel: func() {
u.licenseWindow.Hide()
},
}
cfg.OnLicensed = func() {
// License status has changed, reload the window!
if u.licenseWindow != nil {
u.licenseWindow.Hide()
}
u.licenseWindow = windows.MakeLicenseWindow(d.width, d.height, cfg)
}
cfg.OnLicensed()
}
u.licenseWindow.Show()
})
helpMenu.AddItem("About", func() {
if u.aboutWindow == nil {
u.aboutWindow = windows.NewAboutWindow(windows.About{
@ -747,7 +768,7 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
}
var shareware string
if balance.FreeVersion {
if !license.IsRegistered() {
shareware = " (shareware)"
}
extraLabel := ui.NewLabel(ui.Label{

100
pkg/license/admin.go Normal file
View File

@ -0,0 +1,100 @@
package license
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"time"
"github.com/dgrijalva/jwt-go"
)
// AdminGenerateKeys generates the ECDSA public and private key pair for the admin
// side of creating signed license files.
func AdminGenerateKeys() (*ecdsa.PrivateKey, error) {
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
return privateKey, err
}
// AdminWriteKeys writes the admin signing key to .pem files on disk.
func AdminWriteKeys(key *ecdsa.PrivateKey, privateFile, publicFile string) error {
// Encode the private key to PEM format.
x509Encoded, err := x509.MarshalECPrivateKey(key)
if err != nil {
return err
}
pemEncoded := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Bytes: x509Encoded,
})
// Encode the public key to PEM format.
x509EncodedPub, err := x509.MarshalPKIXPublicKey(key.Public())
if err != nil {
return err
}
pemEncodedPub := pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: x509EncodedPub,
})
// Write the files.
if err := ioutil.WriteFile(privateFile, pemEncoded, 0600); err != nil {
return err
}
if err := ioutil.WriteFile(publicFile, pemEncodedPub, 0644); err != nil {
return err
}
return nil
}
// AdminLoadPrivateKey loads the private key from disk.
func AdminLoadPrivateKey(privateFile string) (*ecdsa.PrivateKey, error) {
// Read the private key file.
pemEncoded, err := ioutil.ReadFile(privateFile)
if err != nil {
return nil, err
}
// Decode the private key.
block, _ := pem.Decode([]byte(pemEncoded))
x509Encoded := block.Bytes
privateKey, _ := x509.ParseECPrivateKey(x509Encoded)
return privateKey, nil
}
// AdminLoadPublicKey loads the private key from disk.
func AdminLoadPublicKey(publicFile string) (*ecdsa.PublicKey, error) {
pemEncodedPub, err := ioutil.ReadFile(publicFile)
if err != nil {
return nil, err
}
// Decode the public key.
blockPub, _ := pem.Decode([]byte(pemEncodedPub))
x509EncodedPub := blockPub.Bytes
genericPublicKey, _ := x509.ParsePKIXPublicKey(x509EncodedPub)
publicKey := genericPublicKey.(*ecdsa.PublicKey)
return publicKey, nil
}
// AdminSignRegistration signs the registration object.
func AdminSignRegistration(key *ecdsa.PrivateKey, reg Registration) (string, error) {
reg.StandardClaims = jwt.StandardClaims{
Issuer: "Maze Admin",
IssuedAt: time.Now().Unix(),
NotBefore: time.Now().Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodES384, reg)
signed, err := token.SignedString(key)
if err != nil {
return "", err
}
return signed, nil
}

25
pkg/license/config.go Normal file
View File

@ -0,0 +1,25 @@
package license
import (
"crypto/ecdsa"
"fmt"
)
// Run-time configuration variables provided by the application.
const pemSigningKey = `-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEMrqAMHjZ1dPlKwDOsiCSr5N3OSvnYKLM
efe2xD+5hJYrpvparRFnaMbMuqde4M6d6sCCKO8BHtfAzmyiQ/CD38zs9MiDsamy
FDYEEJu+Fqx482I7fIa5ZEE770+wWJ3k
-----END PUBLIC KEY-----`
var Signer *ecdsa.PublicKey
func init() {
key, err := ParsePublicKeyPEM(pemSigningKey)
if err != nil {
fmt.Printf("license: failed to parse app keys: %s\n", err)
return
}
Signer = key
}

103
pkg/license/license.go Normal file
View File

@ -0,0 +1,103 @@
// Package license holds functions related to paid product activation.
package license
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
"path/filepath"
"git.kirsle.net/apps/doodle/pkg/userdir"
"github.com/dgrijalva/jwt-go"
)
// Registration object encoded into a license key file.
type Registration struct {
Name string `json:"name"`
Email string `json:"email"`
jwt.StandardClaims
}
// IsRegistered returns a boolean answer: is the product registered?
func IsRegistered() bool {
if _, err := GetRegistration(); err == nil {
return true
}
return false
}
// GetRegistration returns the currently registered user, by checking
// for the license.key file in the profile folder.
func GetRegistration() (Registration, error) {
if Signer == nil {
return Registration{}, errors.New("signer not ready")
}
filename := filepath.Join(userdir.ProfileDirectory, "license.key")
jwt, err := ioutil.ReadFile(filename)
if err != nil {
return Registration{}, err
}
// Check if the JWT is valid.
reg, err := Validate(Signer, string(jwt))
if err != nil {
return Registration{}, err
}
return reg, err
}
// UploadLicenseFile handles the user selecting the license key file, and it is
// validated and ingested.
func UploadLicenseFile(filename string) (Registration, error) {
if Signer == nil {
return Registration{}, errors.New("signer not ready")
}
jwt, err := ioutil.ReadFile(filename)
if err != nil {
return Registration{}, err
}
// Check if the JWT is valid.
reg, err := Validate(Signer, string(jwt))
if err != nil {
return Registration{}, err
}
// Upload the license to Doodle's profile directory.
outfile := filepath.Join(userdir.ProfileDirectory, "license.key")
if err := ioutil.WriteFile(outfile, jwt, 0644); err != nil {
return Registration{}, err
}
return reg, nil
}
// Validate the registration is signed by the appropriate public key.
func Validate(publicKey *ecdsa.PublicKey, tokenString string) (Registration, error) {
var reg Registration
token, err := jwt.ParseWithClaims(tokenString, &reg, func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
if err != nil {
return reg, err
}
if !token.Valid {
return reg, errors.New("token not valid")
}
return reg, nil
}
// ParsePublicKeyPEM loads a public key from PEM format.
func ParsePublicKeyPEM(keytext string) (*ecdsa.PublicKey, error) {
blockPub, _ := pem.Decode([]byte(keytext))
x509EncodedPub := blockPub.Bytes
genericPublicKey, _ := x509.ParsePKIXPublicKey(x509EncodedPub)
publicKey := genericPublicKey.(*ecdsa.PublicKey)
return publicKey, nil
}

View File

@ -6,12 +6,14 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/scripting"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/apps/doodle/pkg/updater"
"git.kirsle.net/apps/doodle/pkg/windows"
"git.kirsle.net/go/render"
"git.kirsle.net/go/render/event"
"git.kirsle.net/go/ui"
@ -30,6 +32,8 @@ type MainScene struct {
labelVersion *ui.Label
labelHint *ui.Label
frame *ui.Frame // Main button frame
btnRegister *ui.Button
winRegister *ui.Window
// Update check variables.
updateButton *ui.Button
@ -63,7 +67,7 @@ func (s *MainScene) Setup(d *Doodle) error {
// Version label.
var shareware string
if balance.FreeVersion {
if !license.IsRegistered() {
shareware = " (shareware)"
}
ver := ui.NewLabel(ui.Label{
@ -106,6 +110,41 @@ func (s *MainScene) Setup(d *Doodle) error {
s.updateButton.Hide()
s.Supervisor.Add(s.updateButton)
// Register button.
s.btnRegister = ui.NewButton("Register", ui.NewLabel(ui.Label{
Text: "Register Game",
Font: balance.LabelFont,
}))
s.btnRegister.SetStyle(&balance.ButtonPrimary)
s.btnRegister.Handle(ui.Click, func(ed ui.EventData) error {
if s.winRegister == nil {
cfg := windows.License{
Supervisor: s.Supervisor,
Engine: d.Engine,
OnCancel: func() {
s.winRegister.Hide()
},
}
cfg.OnLicensed = func() {
// License status has changed, reload the window!
if s.winRegister != nil {
s.winRegister.Hide()
}
s.winRegister = windows.MakeLicenseWindow(d.width, d.height, cfg)
}
cfg.OnLicensed()
}
s.winRegister.Show()
return nil
})
s.btnRegister.Compute(d.Engine)
s.Supervisor.Add(s.btnRegister)
if license.IsRegistered() {
s.btnRegister.Hide()
}
// Main UI button frame.
frame := ui.NewFrame("frame")
s.frame = frame
@ -329,7 +368,7 @@ func (s *MainScene) Draw(d *Doodle) error {
// Hint label.
s.labelHint.MoveTo(render.Point{
X: d.width - s.labelHint.Size().W - 32,
X: (d.width / 2) - (s.labelHint.Size().W / 2),
Y: d.height - s.labelHint.Size().H - 32,
})
s.labelHint.Present(d.Engine, s.labelHint.Point())
@ -348,6 +387,16 @@ func (s *MainScene) Draw(d *Doodle) error {
})
s.frame.Present(d.Engine, s.frame.Point())
// Register button.
s.btnRegister.MoveTo(render.Point{
X: d.width - s.btnRegister.Size().W - 24,
Y: d.height - s.btnRegister.Size().H - 24,
})
s.btnRegister.Present(d.Engine, s.btnRegister.Point())
// Present supervised windows.
s.Supervisor.Present(d.Engine)
return nil
}

275
pkg/windows/license_key.go Normal file
View File

@ -0,0 +1,275 @@
package windows
import (
"fmt"
"time"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/license"
"git.kirsle.net/apps/doodle/pkg/modal"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/style"
)
// License window.
type License struct {
// Settings passed in by doodle
Supervisor *ui.Supervisor
Engine render.Engine
OnLicensed func()
OnCancel func()
}
// MakeLicenseWindow initializes a license window for any scene.
// The window width/height are the actual SDL2 window dimensions.
func MakeLicenseWindow(windowWidth, windowHeight int, cfg License) *ui.Window {
win := NewLicenseWindow(cfg)
win.Compute(cfg.Engine)
win.Supervise(cfg.Supervisor)
// Center the window.
size := win.Size()
win.MoveTo(render.Point{
X: (windowWidth / 2) - (size.W / 2),
Y: (windowHeight / 2) - (size.H / 2),
})
return win
}
// NewLicenseWindow initializes the window.
func NewLicenseWindow(cfg License) *ui.Window {
var (
windowWidth = 340
windowHeight = 320
labelSize = render.NewRect(100, 16)
valueSize = render.NewRect(windowWidth-labelSize.W-4, labelSize.H)
isRegistered bool
registration license.Registration
summary = "Unregistered (shareware)"
)
// Get our current registration status.
if reg, err := license.GetRegistration(); err == nil {
isRegistered = true
registration = reg
windowHeight = 200
summary = "Registered"
}
window := ui.NewWindow("Registration")
window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{
Width: windowWidth,
Height: windowHeight,
Background: render.RGBA(200, 200, 255, 255),
})
var rows = []struct {
IfRegistered bool
IfUnregistered bool
Label string
Text string
Button *ui.Button
ButtonStyle *style.Button
Func func()
PadY int
PadX int
}{
{
Label: "Version:",
Text: fmt.Sprintf("%s v%s", branding.AppName, branding.Version),
},
{
Label: "Status:",
Text: summary,
},
{
IfRegistered: true,
Label: "Name:",
Text: registration.Name,
},
{
IfRegistered: true,
Label: "Email:",
Text: registration.Email,
},
{
IfRegistered: true,
Label: "Issued:",
Text: time.Unix(registration.IssuedAt, 0).Format("Jan 2, 2006 15:04:05 MST"),
},
{
IfUnregistered: true,
Text: "Register your game today! By purchasing the full\n" +
"version of Sketchy Maze, you will unlock additional\n" +
"features including improved support for custom\n" +
"doodads that attach with your level files for easy\n" +
"sharing between multiple computers.\n\n" +
"When you purchase the game you will receive a\n" +
"license key file; click the button below to browse\n" +
"and select the key file to register this copy of\n" +
branding.AppName + ".",
PadY: 8,
PadX: 2,
},
{
IfRegistered: true,
Text: "Thank you for your support!",
PadY: 8,
PadX: 2,
},
{
IfUnregistered: true,
Button: ui.NewButton("Key Browse", ui.NewLabel(ui.Label{
Text: "Upload License Key File",
Font: balance.UIFont,
})),
ButtonStyle: &balance.ButtonPrimary,
Func: func() {
filename, err := native.OpenFile("Select License File", "*.key *.txt")
if err != nil {
modal.Alert(err.Error())
return
}
// Upload and validate the license key.
reg, err := license.UploadLicenseFile(filename)
if err != nil {
modal.Alert(err.Error())
return
}
modal.Alert("Thank you, %s!", reg.Name).WithTitle("Registration OK!")
if cfg.OnLicensed != nil {
cfg.OnLicensed()
}
},
},
}
for _, row := range rows {
row := row
// It has a conditional?
if (row.IfRegistered && !isRegistered) ||
(row.IfUnregistered && isRegistered) {
continue
}
frame := ui.NewFrame("Frame")
if row.Label != "" {
lf := ui.NewFrame("LabelFrame")
lf.Resize(labelSize)
label := ui.NewLabel(ui.Label{
Text: row.Label,
Font: balance.LabelFont,
})
lf.Pack(label, ui.Pack{
Side: ui.E,
})
frame.Pack(lf, ui.Pack{
Side: ui.W,
})
}
if row.Text != "" {
tf := ui.NewFrame("TextFrame")
if row.Label != "" {
tf.Resize(valueSize)
}
label := ui.NewLabel(ui.Label{
Text: row.Text,
Font: balance.UIFont,
})
tf.Pack(label, ui.Pack{
Side: ui.W,
})
frame.Pack(tf, ui.Pack{
Side: ui.W,
})
}
if row.Button != nil {
btn := row.Button
if row.ButtonStyle != nil {
btn.SetStyle(row.ButtonStyle)
}
btn.Handle(ui.Click, func(ed ui.EventData) error {
if row.Func != nil {
row.Func()
}
return nil
})
btn.Compute(cfg.Engine)
cfg.Supervisor.Add(btn)
frame.Pack(btn, ui.Pack{
Side: ui.N,
})
}
window.Pack(frame, ui.Pack{
Side: ui.N,
FillX: true,
PadY: row.PadY,
PadX: row.PadX,
})
}
/////////////
// Buttons at bottom of window
bottomFrame := ui.NewFrame("Button Frame")
window.Pack(bottomFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
frame := ui.NewFrame("Button frame")
buttons := []struct {
label string
f func()
}{
{"Website", func() {
native.OpenURL(branding.Website)
}},
{"Close", func() {
if cfg.OnCancel != nil {
cfg.OnCancel()
}
}},
}
for _, button := range buttons {
button := button
btn := ui.NewButton(button.label, ui.NewLabel(ui.Label{
Text: button.label,
Font: balance.MenuFont,
}))
btn.Handle(ui.Click, func(ed ui.EventData) error {
button.f()
return nil
})
btn.Compute(cfg.Engine)
cfg.Supervisor.Add(btn)
frame.Pack(btn, ui.Pack{
Side: ui.W,
PadX: 4,
Expand: true,
Fill: true,
})
}
bottomFrame.Pack(frame, ui.Pack{
Side: ui.N,
PadX: 8,
PadY: 12,
})
return window
}