M cmd/smscp/main.go => cmd/smscp/main.go +23 -5
@@ 22,7 22,6 @@ import (
const (
// BASE = "http://localhost:3000"
- // BASE = "https://beta.smscp.xyz"
BASE = "https://smscp.xyz"
APILogin = BASE + "/cli/user/login"
APIRegister = BASE + "/cli/user/create"
@@ 189,6 188,13 @@ func login(c *cli.Context) error {
}
func create(c *cli.Context) error {
+ // TODO:
+ // 1. Check if this is a file
+ // a. If so read file.
+ // 2. Check if contents have mime type
+ // a. If so send to server as file.
+ // 3. Otherwise business as usual.
+
usr, err := user.Current()
if err != nil {
return errors.Wrap(err, "failed to retrieve current user from operating system")
@@ 212,10 218,22 @@ func create(c *cli.Context) error {
return err
}
- resp, err := post(APICreate, hash{
- "Token": cfg.Token,
- "Text": string(text),
- })
+ var resp *http.Response
+ // If has a file type other than plain text upload to server as a file.
+ contentType := http.DetectContentType(text)
+ if strings.Contains(contentType, "text/plain") {
+ // Otherwise business as usual.
+ resp, err = post(APICreate, hash{
+ "Token": cfg.Token,
+ "Text": string(text),
+ })
+ } else if contentType != "" {
+ // Upload as file to server.
+ resp, err = post(APICreate, hash{
+ "Token": cfg.Token,
+ "File": string(text),
+ })
+ }
if err != nil {
return errors.Wrap(err, "failed to create request to remote server")
}
A cmd/smscp/smscp => cmd/smscp/smscp +0 -0
M go.mod => go.mod +1 -0
@@ 13,6 13,7 @@ require (
github.com/pkg/errors v0.8.1
github.com/sfreiberg/gotwilio v0.0.0-20191120211240-38187998ae52
github.com/stretchr/testify v1.4.0
+ github.com/stripe/stripe-go v69.4.0+incompatible
github.com/tdewolff/minify v2.3.6+incompatible
github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/test v1.0.6 // indirect
M go.sum => go.sum +2 -0
@@ 131,6 131,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stripe/stripe-go v69.4.0+incompatible h1:KLp4h/eAe0J+FNTZSHh6sVETSKYxSUifumkxJFNMZpg=
+github.com/stripe/stripe-go v69.4.0+incompatible/go.mod h1:A1dQZmO/QypXmsL0T8axYZkSN/uA/T/A64pfKdBAMiY=
github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo=
github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs=
github.com/tdewolff/parse v2.3.4+incompatible h1:x05/cnGwIMf4ceLuDMBOdQ1qGniMoxpP46ghf0Qzh38=
M internal/api/api.go => internal/api/api.go +40 -9
@@ 4,6 4,7 @@ import (
"context"
"fmt"
"io/ioutil"
+ "mime"
"net/http"
"net/url"
"os"
@@ 219,10 220,8 @@ func (app App) NoteCreate(c *gin.Context) {
}
func (app App) NoteCreateCLI(c *gin.Context) {
- // TODO: File support?
-
var payload struct {
- Token, Text string
+ Token, Text, File string
}
err := c.Bind(&payload)
@@ 237,14 236,46 @@ func (app App) NoteCreateCLI(c *gin.Context) {
return
}
- note, err := app.data.NoteCreate(c, user, payload.Text)
- if err != nil {
- app.errorCLI(c, err)
- return
+ var noteText string
+ var mediaURL string
+
+ if payload.Text != "" {
+ note, err := app.data.NoteCreate(c, user, payload.Text)
+ if err != nil {
+ app.errorCLI(c, errors.Wrap(err, "failed to create note"))
+ return
+ }
+ noteText = note.Text()
}
- if err := app.sms.Send(user.Phone(), note.Text(), ""); err != nil {
- app.errorCLI(c, err)
+ if payload.File != "" {
+ contentType := http.DetectContentType([]byte(payload.File))
+ exts, err := mime.ExtensionsByType(contentType)
+ if err != nil {
+ app.errorCLI(c, errors.Wrap(err, "failed to find file type"))
+ return
+ }
+
+ ext := exts[0]
+ fn := fmt.Sprintf("user_upload%s", ext)
+
+ url, err := app.file.Upload([]byte(payload.File), fn)
+ if err != nil {
+ app.errorCLI(c, errors.Wrap(err, "failed to upload file"))
+ return
+ }
+
+ media, err := app.data.MediaCreate(c, user, url)
+ if err != nil {
+ app.errorCLI(c, errors.Wrap(err, "failed to create file record"))
+ return
+ }
+
+ mediaURL = fmt.Sprintf("%s?usertoken=%s", media.Text(), user.Token())
+ }
+
+ if err := app.sms.Send(user.Phone(), noteText, mediaURL); err != nil {
+ app.error(c, errors.Wrap(err, "failed to send text"))
return
}
M internal/common/common.go => internal/common/common.go +5 -0
@@ 14,6 14,11 @@ type User interface {
SetPass(string)
SetPhone(string)
Save(context.Context) error
+
+ // For Stripe.
+ HasPlan(planID string) bool
+ StripeSession() (id string, ok bool)
+ HasStripeSession() bool
}
type Note interface {
M internal/fs/fs.go => internal/fs/fs.go +30 -0
@@ 6,6 6,7 @@ import (
"log"
"os"
"path/filepath"
+ "strings"
"time"
"cloud.google.com/go/firestore"
@@ 306,6 307,11 @@ func (fs FS) NoteCreate(ctx context.Context, user common.User, text string) (com
return fs.noteCreate(ctx, user, text, "")
}
+func (fs FS) UserAddStripePlan(ctx context.Context, stripeSessionID, planID string) error {
+ // TODO: Complete this.
+ return errors.New("NOT COMPLETE")
+}
+
func (fs FS) UserGet(ctx context.Context, token string) (common.User, error) {
claims, err := fs.sec.TokenFrom(token)
if err != nil {
@@ 420,6 426,10 @@ type User struct {
UserEncryptedPassword string
UserCreatedAt int64
+ // For Stripe.
+ UserStripePlans []string
+ UserStripeSession string // The current Stripe session user is in (i.e. currently making payment).
+
// Set when retrieved:
token string
fs FS
@@ 434,6 444,26 @@ func (user *User) Phone() string { return user.UserPhone }
func (user *User) ID() string { return user.ref.ID }
func (user *User) Token() string { return user.token }
+func (user *User) HasPlan(planID string) bool {
+ for _, plan := range user.UserStripePlans {
+ if strings.Contains(plan, planID) {
+ return true
+ }
+ }
+ return false
+}
+
+func (user *User) HasStripeSession() bool {
+ return false
+}
+
+func (user *User) StripeSession() (id string, ok bool) {
+ if user.UserStripeSession == "" {
+ return "", false
+ }
+ return user.UserStripeSession, true
+}
+
func (user *User) SetUsername(value string) {
ctx := context.Background()
A internal/stripe/stripe.go => internal/stripe/stripe.go +77 -0
@@ 0,0 1,77 @@
+package stripe
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/gin-gonic/gin"
+ s "github.com/stripe/stripe-go"
+ "github.com/stripe/stripe-go/checkout/session"
+)
+
+type Stripe struct {
+ key, monthlyBasic, baseURL string
+}
+
+func New(key, monthlyBasic, baseURL string) *Stripe {
+ return &Stripe{
+ key,
+ monthlyBasic,
+ baseURL,
+ }
+}
+
+func (stripe Stripe) CreateMonthlyBascic() (string, error) {
+ p := &s.CheckoutSessionParams{
+ PaymentMethodTypes: s.StringSlice([]string{
+ "card",
+ }),
+ SubscriptionData: &s.CheckoutSessionSubscriptionDataParams{
+ Items: []*s.CheckoutSessionSubscriptionDataItemsParams{
+ &s.CheckoutSessionSubscriptionDataItemsParams{
+ Plan: s.String(stripe.monthlyBasic),
+ },
+ },
+ },
+ // SuccessURL: s.String("https://example.com/success?session_id={CHECKOUT_SESSION_ID}"),
+ SuccessURL: s.String(fmt.Sprintf("%s", stripe.baseURL)),
+ CancelURL: s.String(stripe.baseURL),
+ }
+
+ sesh, err := session.New(p)
+ if err != nil {
+ return "", err
+ }
+
+ return sesh.ID, nil
+}
+
+type HookPayload struct {
+ ID string // This will be same as sesh.ID above.
+ DisplayItems []struct {
+ Plan struct {
+ ID string // This will be the same as monthlyBasic above.
+ }
+ }
+}
+
+func (stripe Stripe) Hook(c *gin.Context) (checkoutSessionID string, paymentPlanIDs []string, err error) {
+ var payload HookPayload
+ err = c.Bind(&payload)
+ if err != nil {
+ return
+ }
+
+ if len(payload.DisplayItems) < 1 {
+ err = errors.New("no purchases")
+ return
+ }
+
+ checkoutSessionID = payload.ID
+
+ for _, item := range payload.DisplayItems {
+ paymentPlanIDs = append(paymentPlanIDs, item.Plan.ID)
+ }
+
+ return checkoutSessionID, paymentPlanIDs, nil
+}
M pkg/builder/builder.go => pkg/builder/builder.go +3 -0
@@ 11,6 11,7 @@ import (
"git.evanjon.es/minieggs/smscp/internal/fs"
"git.evanjon.es/minieggs/smscp/internal/securetoken"
"git.evanjon.es/minieggs/smscp/internal/sms/twilio"
+ "git.evanjon.es/minieggs/smscp/internal/stripe"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
@@ 28,6 29,8 @@ func Build() *gin.Engine {
// User reset forms are only valid for five minutes.
tok := securetoken.New(sec, data, "UserToken", 5*time.Minute)
e3 := e3.New(os.Getenv("E3_USERNAME"), os.Getenv("E3_PASSWORD"))
+ stripe := stripe.New(os.Getenv("STRIPE_KEY"), os.Getenv("STRIPE_MONTHLY_BASIC"), os.Getenv("BASE_URL"))
+ _ = stripe
app := api.AppDefault(data, sms, csv, sec, tok, e3)
// gin
M web/html/_main.js => web/html/_main.js +17 -0
@@ 423,5 423,22 @@
})();
{{ end }}
+ // TODO: Complete.
+ {{ if .User.HasStripeSession }}
+ (function() {
+ var stripeScript = 'https://js.stripe.com/v3/'
+ var el = document.createElement('script')
+ el.setAttribute('src', stripeScript)
+ el.addEventListener('load', function() {
+ stripe.redirectToCheckout({
+ sessionId: '{{ .User.StripeSession }}'
+ }).then(function (result) {
+ console.log({result: result, error: result.error})
+ });
+ })
+ document.body.appendChild(el)
+ })();
+ {{ end }}
+
{{ end }}