From 3334ce52771c370027df325732723247a0b82b34 Mon Sep 17 00:00:00 2001 From: jigar-f <132374182+jigar-f@users.noreply.github.com> Date: Sat, 20 Jul 2024 03:51:33 +0530 Subject: [PATCH] Desktop Username password (#1111) * started working on desktop username passwrord. * Work on onboarding pro user to new flow desktop. * added support for login and logout. * Update account.dart * Added some new setting and persist data * Fixed issue UI not updating. * Started working on reset password flow for desktop. * Validate code on verification. * Enable delete account flow. * Implemented Delete flow. * Started working on deviceLimit flow. * Completed Device Limit flow. * Started working on signupflow. * Add test payment method. * fixed issue with pro and device id. * fixed issue with report parsing issue on reportIssue. * Use compute in reseller code * Update session_model.dart * Fixed issue with salt returning null. * Null check on srp client. * Update user data when renew plans. * change CI branch. * Disable windows CI for now. * PR review changes. * revert CI changes * remove commented lines * remove commented lines * Remove commented code. * fixed signal issues. * Update lib.go * Use ParallelForIdempotent for domain fronting. * use ParallelPreferChained proxy. * Hide auth flow for android. --------- Co-authored-by: atavism --- assets/locales/en-us.po | 2 +- desktop/app/app.go | 24 +- desktop/app/pro.go | 28 +- desktop/lib.go | 534 ++++++++++++++++-- desktop/settings/settings.go | 57 +- go.mod | 1 + go.sum | 2 + internalsdk/auth/auth.go | 8 +- internalsdk/auth/srp.go | 188 ++++++ internalsdk/common/user_config.go | 15 +- internalsdk/pro/pro.go | 4 +- internalsdk/pro/pro_test.go | 56 ++ internalsdk/session_model.go | 95 +--- .../webclient/defaultwebclient/rest.go | 5 +- lib/account/account.dart | 53 +- lib/account/account_management.dart | 9 +- lib/account/auth/create_account_email.dart | 31 - lib/account/auth/verification.dart | 34 +- lib/account/report_issue.dart | 1 + lib/app.dart | 3 + lib/common/session_model.dart | 114 +++- lib/core/router/router.gr.dart | 20 +- lib/ffi.dart | 212 +++++-- lib/generated_bindings.dart | 195 ++++++- lib/home.dart | 6 +- lib/plans/checkout.dart | 269 ++++----- lib/plans/plan_details.dart | 17 +- lib/plans/plans.dart | 23 - lib/plans/utils.dart | 4 +- pubspec.lock | 4 +- pubspec.yaml | 2 +- 31 files changed, 1509 insertions(+), 507 deletions(-) create mode 100644 internalsdk/auth/srp.go create mode 100644 internalsdk/pro/pro_test.go diff --git a/assets/locales/en-us.po b/assets/locales/en-us.po index e1a0849da..f65394137 100644 --- a/assets/locales/en-us.po +++ b/assets/locales/en-us.po @@ -244,7 +244,7 @@ msgstr "Cannot complete purchase" msgid "discover_not_working" -msgstr "Discover not working " +msgstr "Discover not working" msgid "cannot_sign_in" msgstr "Cannot sign in" diff --git a/desktop/app/app.go b/desktop/app/app.go index 6e5affb9d..801026f26 100644 --- a/desktop/app/app.go +++ b/desktop/app/app.go @@ -36,6 +36,7 @@ import ( "github.com/getlantern/profiling" "github.com/getlantern/lantern-client/desktop/analytics" + "github.com/getlantern/lantern-client/desktop/autoupdate" "github.com/getlantern/lantern-client/desktop/datacap" "github.com/getlantern/lantern-client/desktop/features" @@ -376,6 +377,27 @@ func (app *App) SetLanguage(lang string) { } } +func (app *App) SetUserLoggedIn(value bool) { + app.settings.SetUserLoggedIn(value) + if app.ws != nil { + app.ws.SendMessage("pro", map[string]interface{}{ + "login": value, + }) + } +} + +func (app *App) IsUserLoggedIn() bool { + return app.Settings().IsUserLoggedIn() + +} + +// Create func that send message to UI +func (app *App) SendMessageToUI(service string, message interface{}) { + if app.ws != nil { + app.ws.SendMessage(service, message) + } +} + // OnSettingChange sets a callback cb to get called when attr is changed from server. // When calling multiple times for same attr, only the last one takes effect. func (app *App) OnSettingChange(attr settings.SettingName, cb func(interface{})) { @@ -456,8 +478,6 @@ func (app *App) HasSucceedingProxy() bool { } func (app *App) GetHasConfigFetched() bool { - - log.Debugf("Global config fetched: %v, Proxies config fetched: %v") return atomic.LoadInt32(&app.fetchedGlobalConfig) == 1 } diff --git a/desktop/app/pro.go b/desktop/app/pro.go index 1e886e8a8..bc8794e35 100644 --- a/desktop/app/pro.go +++ b/desktop/app/pro.go @@ -113,16 +113,25 @@ func fetchUserDataWithClient(ctx context.Context, proClient pro.ProClient, uc co if err != nil { return nil, err } - setUserData(ctx, userID, resp.User) + SetUserData(ctx, userID, resp.User) log.Debugf("User %d is '%v'", userID, resp.User.UserStatus) return resp, nil } -func setUserData(ctx context.Context, userID int64, user *protos.User) { +func SetUserData(ctx context.Context, userID int64, user *protos.User) { log.Debugf("Storing user data for user %v", userID) userData.save(ctx, userID, user) } +func SetUserDevices(ctx context.Context, userID int64, devices []*protos.Device) { + user, found := userData.get(ctx, userID) + if !found { + return + } + user.Devices = devices + userData.save(ctx, userID, user) +} + // isActive determines whether the given status is an active status func isActive(status string) bool { return status == "active" @@ -140,7 +149,7 @@ func IsProUserFast(ctx context.Context, uc common.UserConfig) (isPro bool, statu if !found { return false, false } - return isActive(user.UserStatus), found + return (isActive(user.UserStatus) || user.UserLevel == "pro"), found } // isProUserFast checks a cached value for the pro status and doesn't wait for @@ -194,18 +203,7 @@ func (app *App) servePro(channel ws.UIChannel) error { } }() - helloFn := func(write func(interface{})) { - if user, known := GetUserDataFast(ctx, app.settings.GetUserID()); known { - log.Debugf("Sending current user data to new client: %v", user) - write(user) - } - log.Debugf("Fetching user data again to see if any changes") - select { - case chFetch <- true: - default: // fetching in progress, skipping - } - } - service, err := channel.Register("pro", helloFn) + service, err := channel.Register("pro", nil) if err != nil { return err } diff --git a/desktop/lib.go b/desktop/lib.go index c96e96f35..450427e2e 100644 --- a/desktop/lib.go +++ b/desktop/lib.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "math/big" "net/http" "os" "os/signal" @@ -28,6 +29,7 @@ import ( "github.com/getlantern/lantern-client/desktop/app" "github.com/getlantern/lantern-client/desktop/autoupdate" "github.com/getlantern/lantern-client/desktop/settings" + "github.com/getlantern/lantern-client/internalsdk/auth" "github.com/getlantern/lantern-client/internalsdk/common" proclient "github.com/getlantern/lantern-client/internalsdk/pro" "github.com/getlantern/lantern-client/internalsdk/protos" @@ -45,9 +47,10 @@ const ( ) var ( - log = golog.LoggerFor("lantern-desktop.main") - a *app.App - proClient proclient.ProClient + log = golog.LoggerFor("lantern-desktop.main") + a *app.App + proClient proclient.ProClient + authClient auth.AuthClient ) var issueMap = map[string]string{ @@ -80,15 +83,17 @@ func start() { cdir := configDir(&flags) settings := loadSettings(cdir) - proClient = proclient.NewClient(fmt.Sprintf("https://%s", common.ProAPIHost), &webclient.Opts{ + webclientOpts := &webclient.Opts{ HttpClient: &http.Client{ - Transport: proxied.ParallelForIdempotent(), + Transport: proxied.ParallelPreferChained(), Timeout: 30 * time.Second, }, UserConfig: func() common.UserConfig { return userConfig(settings) }, - }) + } + proClient = proclient.NewClient(fmt.Sprintf("https://%s", common.ProAPIHost), webclientOpts) + authClient = auth.NewClient(fmt.Sprintf("https://%s", common.V1BaseUrl), webclientOpts) a = app.NewApp(flags, cdir, proClient, settings) go func() { @@ -98,6 +103,8 @@ func start() { } }() + go fetchUserData() + go func() { err := fetchPayentMethodV4() if err != nil { @@ -139,6 +146,93 @@ func start() { }() } +func fetchUserData() error { + user, err := getUserData() + if err != nil { + return log.Errorf("error while fetching user data: %v", err) + } + return cacheUserDetail(user) +} + +func cacheUserDetail(userDetail *protos.User) error { + if userDetail.Email != "" { + a.Settings().SetEmailAddress(userDetail.Email) + } + //Save user refferal code + if userDetail.Referral != "" { + a.SetReferralCode(userDetail.Referral) + } + // err := setUserLevel(session.baseModel, userDetail.UserLevel) + // if err != nil { + // return err + // } + + err := setExpiration(userDetail.Expiration) + if err != nil { + return err + } + currentDevice := getDeviceID() + log.Debugf("Current device %v", currentDevice) + + // Check if device id is connect to same device if not create new user + // this is for the case when user removed device from other device + deviceFound := false + if userDetail.Devices != nil { + for _, device := range userDetail.Devices { + if device.Id == currentDevice { + deviceFound = true + break + } + } + } + log.Debugf("Device found %v", deviceFound) + /// Check if user has installed app first time + firstTime := a.Settings().GetUserFirstVisit() + log.Debugf("First time visit %v", firstTime) + if userDetail.UserLevel == "pro" && firstTime { + log.Debugf("User is pro and first time") + setProUser(true) + } else if userDetail.UserLevel == "pro" && !firstTime && deviceFound { + log.Debugf("User is pro and not first time") + setProUser(true) + } else { + log.Debugf("User is not pro") + setProUser(false) + } + + a.Settings().SetUserIDAndToken(userDetail.UserId, userDetail.Token) + log.Debugf("User caching successful: %+v", userDetail) + // Save data in userData cache + app.SetUserData(context.Background(), userDetail.UserId, userDetail) + return nil +} + +func getDeviceID() string { + return a.Settings().GetDeviceID() +} + +func setExpiration(expiration int64) error { + if expiration == 0 { + return log.Errorf("Expiration date is 0") + } + expiry := time.Unix(0, expiration*int64(time.Second)) + dateFormat := "01/02/2006" + dateStr := expiry.Format(dateFormat) + a.Settings().SetExpirationDate(dateStr) + return nil +} + +func setProUser(isPro bool) { + a.Settings().SetProUser(isPro) + a.SendMessageToUI("pro", map[string]interface{}{ + "isProUser": isPro, + }) +} + +func saveUserSalt(salt []byte) { + a.Settings().SaveSalt(salt) +} + //export hasProxyFected func hasProxyFected() *C.char { if a.GetHasProxyFetched() { @@ -163,19 +257,31 @@ func onSuccess() *C.char { return C.CString(string("false")) } +func userCreate() error { + // User is new + user, err := proClient.UserCreate(context.Background()) + if err != nil { + return errors.New("Could not create new Pro user: %v", err) + } + log.Debugf("DEBUG: User created: %v", user) + if user.BaseResponse != nil && user.BaseResponse.Error != "" { + return errors.New("Could not create new Pro user: %v", err) + } + a.Settings().SetUserIDAndToken(user.UserId, user.Token) + + return nil +} + func fetchOrCreate() error { settings := a.Settings() + settings.SetLanguage("en_us") userID := settings.GetUserID() if userID == 0 { - user, err := proClient.UserCreate(context.Background()) + a.Settings().SetUserFirstVisit(true) + err := userCreate() if err != nil { - return errors.New("Could not create new Pro user: %v", err) + return err } - log.Debugf("DEBUG: User created: %v", user) - if user.BaseResponse != nil && user.BaseResponse.Error != "" { - return errors.New("Could not create new Pro user: %v", err) - } - settings.SetUserIDAndToken(user.UserId, user.Token) // if the user is new mean we need to fetch the payment methods fetchPayentMethodV4() } @@ -191,13 +297,13 @@ func fetchPayentMethodV4() error { if err != nil { return errors.New("Could not get payment methods: %v", err) } - // log.Debugf("DEBUG: Payment methods logos: %v providers %v and plans in string %v", resp.Logo, resp.Providers, resp.Plans) + log.Debugf("DEBUG: Payment methods: %+v", resp) + log.Debugf("DEBUG: Payment methods providers: %+v", resp.Providers) bytes, err := json.Marshal(resp) if err != nil { return errors.New("Could not marshal payment methods: %v", err) } settings.SetPaymentMethodPlans(bytes) - return nil } @@ -268,10 +374,11 @@ func getUserData() (*protos.User, error) { if err != nil { return nil, err } - user := resp.User - if user != nil && user.Email != "" { - a.Settings().SetEmailAddress(user.Email) + if resp.User == nil { + return nil, errors.New("User data not found") } + user := resp.User + cacheUserDetail(user) return user, nil } @@ -292,12 +399,12 @@ func setProxyAll(value *C.char) { // tryCacheUserData retrieves the latest user data for the given user. // It first checks the cache and if present returns the user data stored there -func tryCacheUserData() (*protos.User, error) { - if cacheUserData, isOldFound := cachedUserData(); isOldFound { - return cacheUserData, nil - } - return getUserData() -} +// func tryCacheUserData() (*protos.User, error) { +// if cacheUserData, isOldFound := cachedUserData(); isOldFound { +// return cacheUserData, nil +// } +// return getUserData() +// } // this method is reposible for checking if the user has updated plan or bought plans // @@ -314,6 +421,8 @@ func hasPlanUpdatedOrBuy() *C.char { if isOldFound { if cacheUserData.Expiration < resp.User.Expiration { // New data has a later expiration + // if foud then update the cache + cacheUserDetail(resp.User) return C.CString(string("true")) } } @@ -322,9 +431,11 @@ func hasPlanUpdatedOrBuy() *C.char { //export devices func devices() *C.char { - user, err := tryCacheUserData() - if err != nil { - return sendError(err) + user, found := cachedUserData() + if !found { + // for now just return empty array + b, _ := json.Marshal("[]") + return C.CString(string(b)) } b, _ := json.Marshal(user.Devices) return C.CString(string(b)) @@ -365,9 +476,9 @@ func removeDevice(deviceId *C.char) *C.char { //export expiryDate func expiryDate() *C.char { - user, err := tryCacheUserData() - if err != nil { - return sendError(err) + user, found := cachedUserData() + if !found { + return sendError(log.Errorf("User data not found")) } tm := time.Unix(user.Expiration, 0) exp := tm.Format("01/02/2006") @@ -413,6 +524,23 @@ func emailExists(email *C.char) *C.char { return C.CString("false") } +//export testProviderRequest +func testProviderRequest(email *C.char, paymentProvider *C.char, plan *C.char) *C.char { + puchaseData := map[string]interface{}{ + "idempotencyKey": strconv.FormatInt(time.Now().UnixNano(), 10), + "provider": C.GoString(paymentProvider), + "email": C.GoString(email), + "plan": C.GoString(plan), + } + _, err := proClient.PurchaseRequest(context.Background(), puchaseData) + if err != nil { + return sendError(err) + } + setProUser(true) + getUserData() + return C.CString("true") +} + // The function returns two C strings: the first represents success, and the second represents an error. // If the redemption is successful, the first string contains "true", and the second string is nil. // If an error occurs during redemption, the first string is nil, and the second string contains the error message. @@ -449,6 +577,12 @@ func referral() *C.char { return C.CString(referralCode) } +//export myDeviceId +func myDeviceId() *C.char { + deviceId := getDeviceID() + return C.CString(deviceId) +} + //export chatEnabled func chatEnabled() *C.char { return C.CString("false") @@ -528,13 +662,15 @@ func acceptedTermsVersion() *C.char { //export proUser func proUser() *C.char { - ctx := context.Background() - // refresh user data when home page is loaded on desktop - go getUserData() + // // refresh user data when home page is loaded on desktop + // go getUserData() uc := a.Settings() - if isProUser, ok := app.IsProUserFast(ctx, uc); isProUser && ok { + if uc.IsProUser() { return C.CString("true") } + // if isProUser, ok := app.IsProUserFast(ctx, uc); isProUser && ok { + // return C.CString("true") + // } return C.CString("false") } @@ -607,12 +743,14 @@ func userConfig(settings *settings.Settings) common.UserConfig { } //export reportIssue -func reportIssue(email, issueType, description *C.char) (*C.char, *C.char) { +func reportIssue(email, issueType, description *C.char) *C.char { + issueTypeStr := C.GoString(issueType) deviceID := a.Settings().GetDeviceID() - issueIndex := issueMap[C.GoString(issueType)] + issueIndex := issueMap[issueTypeStr] issueTypeInt, err := strconv.Atoi(issueIndex) if err != nil { - return nil, sendError(err) + log.Errorf("Error converting issue type to int: %v", err) + return sendError(err) } ctx := context.Background() uc := userConfig(a.Settings()) @@ -641,10 +779,10 @@ func reportIssue(email, issueType, description *C.char) (*C.char, *C.char) { nil, ) if err != nil { - return nil, sendError(err) + return sendError(err) } log.Debug("Successfully reported issue") - return C.CString("true"), nil + return C.CString("true") } //export checkUpdates @@ -746,4 +884,322 @@ func handleSignals(a *app.App) { }() } +// Auth Methods + +//export isUserFirstTime +func isUserFirstTime() *C.char { + firstVist := a.Settings().GetUserFirstVisit() + stringValue := fmt.Sprintf("%t", firstVist) + return C.CString(stringValue) +} + +//export setFirstTimeVisit +func setFirstTimeVisit() { + a.Settings().SetUserFirstVisit(false) +} + +//export isUserLoggedIn +func isUserLoggedIn() *C.char { + loggedIn := a.IsUserLoggedIn() + stringValue := fmt.Sprintf("%t", loggedIn) + log.Debugf("User logged in %v", stringValue) + return C.CString(stringValue) +} + +func getUserSalt(email string) ([]byte, error) { + lowerCaseEmail := strings.ToLower(email) + salt := a.Settings().GetSalt() + if len(salt) == 16 { + log.Debugf("salt return from cache %v", salt) + return salt, nil + } + log.Debugf("Salt not found calling api for %s", email) + saltResponse, err := authClient.GetSalt(context.Background(), lowerCaseEmail) + if err != nil { + return nil, err + } + log.Debugf("Salt Response-> %v", saltResponse.Salt) + return saltResponse.Salt, nil + +} + +// Authenticates the user with the given email and password. +// +// Note-: On Sign up Client needed to generate 16 byte slat +// Then use that salt, password and email generate encryptedKey once you created encryptedKey pass it to srp.NewSRPClient +// Then use srpClient.Verifier() to generate verifierKey + +//export signup +func signup(email *C.char, password *C.char) *C.char { + lowerCaseEmail := strings.ToLower(C.GoString(email)) + + salt, err := authClient.SignUp(lowerCaseEmail, C.GoString(password)) + if err != nil { + return sendError(err) + } + // save salt and email in settings + setting := a.Settings() + saveUserSalt(salt) + setting.SetEmailAddress(C.GoString(email)) + a.SetUserLoggedIn(true) + + // Todo remove this once we complete teting auth flow + // we don't need this on prod + fetchPayentMethodV4() + return C.CString("true") +} + +//export login +func login(email *C.char, password *C.char) *C.char { + lowerCaseEmail := strings.ToLower(C.GoString(email)) + user, salt, err := authClient.Login(lowerCaseEmail, C.GoString(password), getDeviceID()) + if err != nil { + return sendError(err) + } + // User has more than 3 device connected to device + if !user.Success { + err := deviceLimitFlow(user) + if err != nil { + return sendError(log.Errorf("error while starting device limit flow %v", err)) + } + return sendError(log.Errorf("too-many-devices %v", err)) + } + + log.Debugf("User login successfull %+v", user) + // save salt and email in settings + saveUserSalt(salt) + a.SetUserLoggedIn(true) + userData := auth.ConvertToUserDetailsResponse(user) + // once login is successfull save user details + // but overide there email with login email + // old email might be differnt but we want to show latets email + userData.Email = C.GoString(email) + err = cacheUserDetail(userData) + if err != nil { + return sendError(err) + } + return C.CString("true") +} + +//export logout +func logout() *C.char { + email := a.Settings().GetEmailAddress() + deviceId := getDeviceID() + token := a.Settings().GetToken() + userId := a.Settings().GetUserID() + + signoutData := &protos.LogoutRequest{ + Email: email, + DeviceId: deviceId, + LegacyToken: token, + LegacyUserID: userId, + } + log.Debugf("Sign out request %+v", signoutData) + loggedOut, logoutErr := authClient.SignOut(context.Background(), signoutData) + if logoutErr != nil { + return sendError(log.Errorf("Error while signing out %v", logoutErr)) + } + if !loggedOut { + return sendError(log.Error("Error while signing out")) + } + + clearLocalUserData() + // Create new user + err := userCreate() + if err != nil { + return sendError(err) + } + return C.CString("true") +} + +// User has reached device limit +// Save latest device +func deviceLimitFlow(login *protos.LoginResponse) error { + var protoDevices []*protos.Device + for _, device := range login.Devices { + protoDevice := &protos.Device{ + Id: device.Id, + Name: device.Name, + Created: device.Created, + } + protoDevices = append(protoDevices, protoDevice) + } + + user := &protos.User{ + UserId: login.LegacyID, + Token: login.LegacyToken, + Devices: protoDevices, + } + + app.SetUserData(context.Background(), login.LegacyID, user) + return nil +} + +// Send recovery code to user email +// +//export startRecoveryByEmail +func startRecoveryByEmail(email *C.char) *C.char { + //Create body + lowerCaseEmail := strings.ToLower(C.GoString(email)) + prepareRequestBody := &protos.StartRecoveryByEmailRequest{ + Email: lowerCaseEmail, + } + recovery, err := authClient.StartRecoveryByEmail(context.Background(), prepareRequestBody) + if err != nil { + return sendError(err) + } + log.Debugf("StartRecoveryByEmail response %v", recovery) + return C.CString("true") +} + +// Complete recovery by email +// +//export completeRecoveryByEmail +func completeRecoveryByEmail(email *C.char, code *C.char, password *C.char) *C.char { + //Create body + lowerCaseEmail := strings.ToLower(C.GoString(email)) + newsalt, err := auth.GenerateSalt() + if err != nil { + return sendError(err) + } + log.Debugf("Slat %v and length %v", newsalt, len(newsalt)) + srpClient := auth.NewSRPClient(lowerCaseEmail, C.GoString(password), newsalt) + verifierKey, err := srpClient.Verifier() + if err != nil { + return sendError(err) + } + prepareRequestBody := &protos.CompleteRecoveryByEmailRequest{ + Email: lowerCaseEmail, + Code: C.GoString(code), + NewSalt: newsalt, + NewVerifier: verifierKey.Bytes(), + } + + log.Debugf("new Verifier %v and salt %v", verifierKey.Bytes(), newsalt) + recovery, err := authClient.CompleteRecoveryByEmail(context.Background(), prepareRequestBody) + if err != nil { + return sendError(err) + } + //User has been recovered successfully + //Save new salt + saveUserSalt(newsalt) + log.Debugf("CompleteRecoveryByEmail response %v", recovery) + return C.CString("true") +} + +// // This will validate code send by server +// +//export validateRecoveryByEmail +func validateRecoveryByEmail(email *C.char, code *C.char) *C.char { + lowerCaseEmail := strings.ToLower(C.GoString(email)) + prepareRequestBody := &protos.ValidateRecoveryCodeRequest{ + Email: lowerCaseEmail, + Code: C.GoString(code), + } + recovery, err := authClient.ValidateEmailRecoveryCode(context.Background(), prepareRequestBody) + if err != nil { + return sendError(err) + } + if !recovery.Valid { + return sendError(log.Errorf("invalid_code Error: %v", err)) + } + log.Debugf("Validate code response %v", recovery.Valid) + return C.CString("true") +} + +// This will delete user accoutn and creates new user +// +//export deleteAccount +func deleteAccount(password *C.char) *C.char { + email := a.Settings().GetEmailAddress() + lowerCaseEmail := strings.ToLower(email) + // Get the salt + salt, err := getUserSalt(lowerCaseEmail) + if err != nil { + return sendError(err) + } + // Prepare login request body + client := auth.NewSRPClient(lowerCaseEmail, C.GoString(password), salt) + + //Send this key to client + A := client.EphemeralPublic() + //Create body + prepareRequestBody := &protos.PrepareRequest{ + Email: lowerCaseEmail, + A: A.Bytes(), + } + log.Debugf("Delete Account request email %v A %v", lowerCaseEmail, A.Bytes()) + srpB, err := authClient.LoginPrepare(context.Background(), prepareRequestBody) + if err != nil { + return sendError(err) + } + log.Debugf("Login prepare response %v", srpB.B) + + // // Once the client receives B from the server Client should check error status here as defense against + // // a malicious B sent from server + B := big.NewInt(0).SetBytes(srpB.B) + + if err = client.SetOthersPublic(B); err != nil { + log.Errorf("Error while setting srpB %v", err) + return sendError(err) + } + + // client can now make the session key + clientKey, err := client.Key() + if err != nil || clientKey == nil { + return sendError(log.Errorf("user_not_found error while generating Client key %v", err)) + } + + // // Step 3 + + // // check if the server proof is valid + if !client.GoodServerProof(salt, lowerCaseEmail, srpB.Proof) { + return sendError(log.Error("user_not_found error while checking server proof")) + } + + clientProof, err := client.ClientProof() + if err != nil { + return sendError(log.Errorf("user_not_found error while generating client proof %v", err)) + } + deviceId := a.Settings().GetDeviceID() + + changeEmailRequestBody := &protos.DeleteUserRequest{ + Email: lowerCaseEmail, + Proof: clientProof, + Permanent: true, + DeviceId: deviceId, + } + + log.Debugf("Delete Account request email %v prooof %v deviceId %v", lowerCaseEmail, clientProof, deviceId) + isAccountDeleted, err := authClient.DeleteAccount(context.Background(), changeEmailRequestBody) + if err != nil { + return sendError(err) + } + log.Debugf("Account Delted response %v", isAccountDeleted) + + if !isAccountDeleted { + return sendError(log.Errorf("user_not_found error while deleting account %v", err)) + } + + // Clear local user data + clearLocalUserData() + // Set user id and token to nil + a.Settings().SetUserIDAndToken(0, "") + // Create new user + err = userCreate() + if err != nil { + return sendError(err) + } + return C.CString("true") +} + +// clearLocalUserData clears the local user data from the settings +func clearLocalUserData() { + setting := a.Settings() + saveUserSalt([]byte{}) + setting.SetEmailAddress("") + a.SetUserLoggedIn(false) + setProUser(false) +} + func main() {} diff --git a/desktop/settings/settings.go b/desktop/settings/settings.go index a925b1fd2..9d5920a96 100644 --- a/desktop/settings/settings.go +++ b/desktop/settings/settings.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" "io" - "io/ioutil" + "log" "os" "reflect" "strings" @@ -42,6 +42,7 @@ const ( SNEmailAddress SettingName = "emailAddress" SNUserID SettingName = "userID" SNUserToken SettingName = "userToken" + SNUserPro SettingName = "userPro" SNMigratedDeviceIDForUserID SettingName = "migratedDeviceIDForUserID" SNTakenSurveys SettingName = "takenSurveys" SNPastAnnouncements SettingName = "pastAnnouncements" @@ -56,6 +57,11 @@ const ( SNEnabledExperiments SettingName = "enabledExperiments" SNPaymentMethods SettingName = "paymentMethods" + // Auth methods + SNUserFirstVisit SettingName = "userFirstVisit" + SNExpiryDate SettingName = "expirydate" + SNUserLoggedIn SettingName = "userLoggedIn" + SNSalt SettingName = "salt" ) type settingType byte @@ -99,6 +105,11 @@ var settingMeta = map[SettingName]struct { SNRevisionDate: {stString, false, false}, SNEnabledExperiments: {stStringArray, false, false}, + + //Auth releated + SNUserFirstVisit: {stBool, true, true}, + SNUserLoggedIn: {stBool, true, true}, + SNSalt: {stString, true, true}, } // Settings is a struct of all settings unique to this particular Lantern instance. @@ -121,7 +132,7 @@ func LoadSettingsFrom(version, revisionDate, buildDate, path string) *Settings { set := sett.m // Use settings from disk if they're available. - if bytes, err := ioutil.ReadFile(path); err != nil { + if bytes, err := os.ReadFile(path); err != nil { sett.log.Debugf("Could not read file %v", err) } else if err := yaml.Unmarshal(bytes, set); err != nil { sett.log.Errorf("Could not load yaml %v", err) @@ -184,6 +195,9 @@ func newSettings(filePath string) *Settings { SNUserToken: "", SNUIAddr: "", SNMigratedDeviceIDForUserID: int64(0), + SNUserLoggedIn: false, + SNUserFirstVisit: false, + SNSalt: "", }, filePath: filePath, changeNotifiers: make(map[SettingName][]func(interface{})), @@ -717,3 +731,42 @@ func (s *Settings) onChange(attr SettingName, value interface{}) { wsOut <- s.uiMap() } } + +func (s *Settings) SetProUser(value bool) { + s.setVal(SNUserPro, value) +} + +func (s *Settings) IsProUser() bool { + return s.getBool(SNUserPro) +} + +// Auth methods +// SetUserFirstVisit sets the user's first visit flag +func (s *Settings) SetUserFirstVisit(value bool) { + s.setVal(SNUserFirstVisit, value) +} + +// GetUserFirstVisit returns the user's first visit flag +func (s *Settings) GetUserFirstVisit() bool { + return s.getBool(SNUserFirstVisit) +} + +func (s *Settings) SetExpirationDate(date string) { + s.setVal(SNExpiryDate, date) +} + +func (s *Settings) IsUserLoggedIn() bool { + return s.getBool(SNUserLoggedIn) +} +func (s *Settings) SetUserLoggedIn(value bool) { + log.Println("Setting user logged in to ", value) + s.setVal(SNUserLoggedIn, value) +} + +func (s *Settings) GetSalt() []byte { + return s.getbytes(SNSalt) +} + +func (s *Settings) SaveSalt(salt []byte) { + s.setVal(SNSalt, salt) +} diff --git a/go.mod b/go.mod index 6e3f1a061..a41d72cb0 100644 --- a/go.mod +++ b/go.mod @@ -229,6 +229,7 @@ require ( github.com/mitchellh/go-ps v1.0.0 // indirect github.com/mitchellh/go-server-timing v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moul/http2curl v1.0.0 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/nwaples/rardecode v1.1.2 // indirect github.com/onsi/ginkgo/v2 v2.16.0 // indirect diff --git a/go.sum b/go.sum index df7e23eec..8cc2e2f69 100644 --- a/go.sum +++ b/go.sum @@ -684,6 +684,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk= github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= diff --git a/internalsdk/auth/auth.go b/internalsdk/auth/auth.go index 2c92111d2..a114a7317 100644 --- a/internalsdk/auth/auth.go +++ b/internalsdk/auth/auth.go @@ -25,14 +25,14 @@ type authClient struct { type AuthClient interface { //Sign up methods - SignUp(ctx context.Context, signupData *protos.SignupRequest) (bool, error) + SignUp(email string, password string) ([]byte, error) SignupEmailResendCode(ctx context.Context, data *protos.SignupEmailResendRequest) (bool, error) SignupEmailConfirmation(ctx context.Context, data *protos.ConfirmSignupRequest) (bool, error) //Login methods GetSalt(ctx context.Context, email string) (*protos.GetSaltResponse, error) LoginPrepare(ctx context.Context, loginData *protos.PrepareRequest) (*protos.PrepareResponse, error) - Login(ctx context.Context, loginData *protos.LoginRequest) (*protos.LoginResponse, error) + Login(email string, password string, deviceId string) (*protos.LoginResponse, []byte, error) // Recovery methods StartRecoveryByEmail(ctx context.Context, loginData *protos.StartRecoveryByEmailRequest) (bool, error) CompleteRecoveryByEmail(ctx context.Context, loginData *protos.CompleteRecoveryByEmailRequest) (bool, error) @@ -94,7 +94,7 @@ func (c *authClient) GetSalt(ctx context.Context, email string) (*protos.GetSalt // Sign up API // SignUp is used to sign up a new user with the SignupRequest -func (c *authClient) SignUp(ctx context.Context, signupData *protos.SignupRequest) (bool, error) { +func (c *authClient) signUp(ctx context.Context, signupData *protos.SignupRequest) (bool, error) { var resp protos.EmptyResponse err := c.webclient.PostPROTOC(ctx, "/users/signup", nil, signupData, &resp) if err != nil { @@ -137,7 +137,7 @@ func (c *authClient) LoginPrepare(ctx context.Context, loginData *protos.Prepare } // Login is used to login a user with the LoginRequest -func (c *authClient) Login(ctx context.Context, loginData *protos.LoginRequest) (*protos.LoginResponse, error) { +func (c *authClient) login(ctx context.Context, loginData *protos.LoginRequest) (*protos.LoginResponse, error) { var resp protos.LoginResponse err := c.webclient.PostPROTOC(ctx, "/users/login", nil, loginData, &resp) if err != nil { diff --git a/internalsdk/auth/srp.go b/internalsdk/auth/srp.go new file mode 100644 index 000000000..d9a2186df --- /dev/null +++ b/internalsdk/auth/srp.go @@ -0,0 +1,188 @@ +package auth + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "errors" + "math/big" + "strings" + + "github.com/1Password/srp" + "github.com/getlantern/lantern-client/internalsdk/protos" + "golang.org/x/crypto/pbkdf2" +) + +const ( + group = srp.RFC5054Group3072 +) + +func NewSRPClient(email string, password string, salt []byte) *srp.SRP { + if len(salt) == 0 || len(password) == 0 || len(email) == 0 { + log.Errorf("salt, password and email should not be empty %v %v %v", salt, password, email) + return nil + } + log.Debugf("NewSRPClient email %v password %v salt %v", email, password, salt) + lowerCaseEmail := strings.ToLower(email) + encryptedKey := GenerateEncryptedKey(password, lowerCaseEmail, salt) + log.Debugf("Encrypted key %v", encryptedKey) + return srp.NewSRPClient(srp.KnownGroups[group], encryptedKey, nil) +} + +func ConvertToUserDetailsResponse(userResponse *protos.LoginResponse) *protos.User { + // Convert protobuf to usre details struct + log.Debugf("ConvertToUserDetailsResponse %+v", userResponse) + + user := userResponse.LegacyUserData + + userData := protos.User{ + UserId: userResponse.LegacyUserData.UserId, + Code: user.Code, + Token: userResponse.LegacyToken, + Referral: user.Code, + UserLevel: user.UserLevel, + Expiration: user.Expiration, + Email: user.Email, + UserStatus: user.UserStatus, + Locale: user.Locale, + YinbiEnabled: user.YinbiEnabled, + Inviters: user.Inviters, + Invitees: user.Invitees, + // Purchases: user.Purchases, + } + log.Debugf("ConvertToUserDetailsResponse %+v", &userData) + + for _, d := range user.Devices { + // Map the fields from LoginResponse_Device to UserDevice + userDevice := &protos.Device{ + Id: d.Id, + Name: d.GetName(), + Created: d.GetCreated(), + } + userData.Devices = append(userData.Devices, userDevice) + } + + return &userData +} + +// Takes password and email, salt and returns encrypted key +func GenerateEncryptedKey(password string, email string, salt []byte) *big.Int { + if len(salt) == 0 || len(password) == 0 || len(email) == 0 { + log.Errorf("slat or password or email is empty %v %v %v", salt, password, email) + return nil + } + lowerCaseEmail := strings.ToLower(email) + combinedInput := password + lowerCaseEmail + encryptedKey := pbkdf2.Key([]byte(combinedInput), salt, 4096, 32, sha256.New) + encryptedKeyBigInt := big.NewInt(0).SetBytes(encryptedKey) + return encryptedKeyBigInt +} + +func (c *authClient) getUserSalt(email string) ([]byte, error) { + lowerCaseEmail := strings.ToLower(email) + log.Debugf("Salt not found calling api for %s", email) + salt, err := c.GetSalt(context.Background(), lowerCaseEmail) + if err != nil { + return nil, err + } + log.Debugf("Salt Response-> %v", salt.Salt) + return salt.Salt, nil +} + +func GenerateSalt() ([]byte, error) { + salt := make([]byte, 16) + if n, err := rand.Read(salt); err != nil { + return nil, err + } else if n != 16 { + return nil, errors.New("failed to generate 16 byte salt") + } + return salt, nil +} + +func (c *authClient) SignUp(email string, password string) ([]byte, error) { + lowerCaseEmail := strings.ToLower(email) + salt, err := GenerateSalt() + if err != nil { + return nil, err + } + + srpClient := NewSRPClient(lowerCaseEmail, password, salt) + verifierKey, err := srpClient.Verifier() + if err != nil { + return nil, err + } + signUpRequestBody := &protos.SignupRequest{ + Email: lowerCaseEmail, + Salt: salt, + Verifier: verifierKey.Bytes(), + SkipEmailConfirmation: true, + } + log.Debugf("Sign up request email %v, salt %v verifier %v verifiter in bytes %v", lowerCaseEmail, salt, verifierKey, verifierKey.Bytes()) + signupResponse, err := c.signUp(context.Background(), signUpRequestBody) + if err != nil { + return nil, err + } + log.Debugf("sign up response %v", signupResponse) + return salt, nil +} + +// Todo find way to optimize this method +func (c *authClient) Login(email string, password string, deviceId string) (*protos.LoginResponse, []byte, error) { + lowerCaseEmail := strings.ToLower(email) + // Get the salt + salt, err := c.getUserSalt(lowerCaseEmail) + if err != nil { + return nil, nil, err + } + + // Prepare login request body + client := NewSRPClient(lowerCaseEmail, password, salt) + //Send this key to client + A := client.EphemeralPublic() + //Create body + prepareRequestBody := &protos.PrepareRequest{ + Email: lowerCaseEmail, + A: A.Bytes(), + } + log.Debugf("Login prepare request email %v A %v", lowerCaseEmail, A.Bytes()) + srpB, err := c.LoginPrepare(context.Background(), prepareRequestBody) + if err != nil { + return nil, nil, err + } + log.Debugf("Login prepare response %v", srpB) + + // // Once the client receives B from the server Client should check error status here as defense against + // // a malicious B sent from server + B := big.NewInt(0).SetBytes(srpB.B) + + if err = client.SetOthersPublic(B); err != nil { + log.Errorf("Error while setting srpB %v", err) + return nil, nil, err + } + + // client can now make the session key + clientKey, err := client.Key() + if err != nil || clientKey == nil { + return nil, nil, log.Errorf("user_not_found error while generating Client key %v", err) + } + + // // Step 3 + + // // check if the server proof is valid + if !client.GoodServerProof(salt, lowerCaseEmail, srpB.Proof) { + return nil, nil, log.Errorf("user_not_found error while checking server proof%v", err) + } + + clientProof, err := client.ClientProof() + if err != nil { + return nil, nil, log.Errorf("user_not_found error while generating client proof %v", err) + } + loginRequestBody := &protos.LoginRequest{ + Email: lowerCaseEmail, + Proof: clientProof, + DeviceId: deviceId, + } + log.Debugf("Login request body %v", loginRequestBody) + resp, err := c.login(context.Background(), loginRequestBody) + return resp, salt, err +} diff --git a/internalsdk/common/user_config.go b/internalsdk/common/user_config.go index 276f2110b..23ed60398 100644 --- a/internalsdk/common/user_config.go +++ b/internalsdk/common/user_config.go @@ -32,11 +32,16 @@ type UserConfigData struct { Headers map[string]string } -func (uc *UserConfigData) GetAppName() string { return uc.AppName } -func (uc *UserConfigData) GetDeviceID() string { return uc.DeviceID } -func (uc *UserConfigData) GetUserID() int64 { return uc.UserID } -func (uc *UserConfigData) GetToken() string { return uc.Token } -func (uc *UserConfigData) GetLanguage() string { return uc.Language } +func (uc *UserConfigData) GetAppName() string { return uc.AppName } +func (uc *UserConfigData) GetDeviceID() string { return uc.DeviceID } +func (uc *UserConfigData) GetUserID() int64 { return uc.UserID } +func (uc *UserConfigData) GetToken() string { return uc.Token } +func (uc *UserConfigData) GetLanguage() string { + if uc.Language != "" { + return uc.Language + } + return "en_Us" +} func (uc *UserConfigData) GetTimeZone() (string, error) { return timezone.IANANameForTime(time.Now()) } func (uc *UserConfigData) GetEnabledExperiments() []string { return nil } func (uc *UserConfigData) GetInternalHeaders() map[string]string { diff --git a/internalsdk/pro/pro.go b/internalsdk/pro/pro.go index 2230cf87f..780ad7632 100644 --- a/internalsdk/pro/pro.go +++ b/internalsdk/pro/pro.go @@ -329,9 +329,9 @@ func (c *proClient) UserLinkValidate(ctx context.Context, code string) (*UserRec } // PurchaseRequest is used to request a purchase of a Pro plan is will be used for all most all the payment providers -func (c *proClient) PurchaseRequest(ctx context.Context, data map[string]interface{}) (*PurchaseResponse, error) { +func (c *proClient) PurchaseRequest(ctx context.Context, req map[string]interface{}) (*PurchaseResponse, error) { var resp PurchaseResponse - err := c.webclient.PostJSONReadingJSON(ctx, "/purchase", data, nil, &resp) + err := c.webclient.PostFormReadingJSON(ctx, "/purchase", req, &resp) if err != nil { return nil, err } diff --git a/internalsdk/pro/pro_test.go b/internalsdk/pro/pro_test.go new file mode 100644 index 000000000..d8d564258 --- /dev/null +++ b/internalsdk/pro/pro_test.go @@ -0,0 +1,56 @@ +package pro + +import ( + "context" + "fmt" + "net/http" + "strconv" + "testing" + "time" + + "github.com/getlantern/lantern-client/internalsdk/common" + "github.com/getlantern/lantern-client/internalsdk/webclient" + "github.com/stretchr/testify/assert" +) + +func TestPurchaseRequest(t *testing.T) { + // Write your test code here + // dialTimeout := 30 * time.Second + // curl -X 'POST' -d '' -H 'Access-Control-Allow-Headers: X-Lantern-Device-Id, X-Lantern-Pro-Token, X-Lantern-User-Id' -H 'Content-Type: application/x-www-form-urlencoded' -H 'User-Agent: Lantern/7.6.87 (darwin/arm64) go-resty/2.13.1 (https://github.com/go-resty/resty)' -H 'X-Lantern-App: Lantern' -H 'X-Lantern-App-Version: 9999.99.99' -H 'X-Lantern-Device-Id: 81004e7f-c135-4fd2-96fd-588895b5fcd3' -H 'X-Lantern-Locale: en_US' -H 'X-Lantern-Platform: darwin' -H 'X-Lantern-Pro-Token: fFNNcUAf20uQdZ6ay_mZd9WkwUu22iD7hPQVxBDBhrTWOvwHS7T8ZQ' -H 'X-Lantern-Rand: mjatfmyzfzkuemszhlkixpzewbufcebocibycyqeqfquthvfktwqaqnpbrdsckdtlp' -H 'X-Lantern-Supported-Data-Caps: monthly weekly daily' -H 'X-Lantern-Time-Zone: Asia/Colombo' -H 'X-Lantern-User-Id: 372759893' -H 'X-Lantern-Version: 7.6.87' 'https://api.getiantem.org/purchase' + webclientOpts := &webclient.Opts{ + // Use proxied.Fronted for IOS client since ChainedThenFronted it does not work with ios due to (chained proxy unavailable) + // because we are not using the flashlight on ios + // We need to figure out where to put proxied SetProxyAddr + + HttpClient: &http.Client{}, + UserConfig: func() common.UserConfig { + deviceID := "81004e7f-c135-4fd2-96fd-588895b5fcd3" + userID := int64(372759893) + token := "fFNNcUAf20uQdZ6ay_mZd9WkwUu22iD7hPQVxBDBhrTWOvwHS7T8ZQ" + lang := "en_US" + return common.NewUserConfig( + common.DefaultAppName, + deviceID, + userID, + token, + nil, + lang, + ) + }, + } + + proClient := NewClient(fmt.Sprintf("https://%s", common.ProAPIHost), webclientOpts) + + puchaseData := map[string]interface{}{ + "idempotencyKey": strconv.FormatInt(time.Now().UnixNano(), 10), + "provider": "test", + "email": "jigar+macos+test@getlantern.org", + "plan": "1y-usd", + } + log.Debugf("DEBUG: Testing provider request: %v", puchaseData) + _, err := proClient.PurchaseRequest(context.Background(), puchaseData) + + assert.NoError(t, err) +} + +// Add more test functions as needed diff --git a/internalsdk/session_model.go b/internalsdk/session_model.go index 74c8d5b39..475fe41d2 100644 --- a/internalsdk/session_model.go +++ b/internalsdk/session_model.go @@ -1045,17 +1045,6 @@ func cacheUserDetail(session *SessionModel, userDetail *protos.User) error { } } log.Debugf("Device found %v", deviceFound) - // if !deviceFound { - // // Device has not found in the list - // // Switch to free user - // signOut(*session) - // log.Debugf("Device has not found in the list creating new user") - // err = session.userCreate(context.Background()) - // if err != nil { - // return err - // } - // return nil - // } /// Check if user has installed app first time firstTime, err := checkFirstTimeVisit(session.baseModel) @@ -1063,7 +1052,6 @@ func cacheUserDetail(session *SessionModel, userDetail *protos.User) error { log.Debugf("Error while checking first time visit %v", err) } log.Debugf("First time visit %v", firstTime) - if userDetail.UserLevel == "pro" && firstTime { log.Debugf("User is pro and first time") setProUser(session.baseModel, true) @@ -1208,32 +1196,10 @@ func submitApplePayPayment(m *SessionModel, email string, planId string, purchas // Then use srpClient.Verifier() to generate verifierKey func signup(session *SessionModel, email string, password string) error { lowerCaseEmail := strings.ToLower(email) - err := setEmail(session.baseModel, lowerCaseEmail) - if err != nil { - return err - } - salt, err := GenerateSalt() + salt, err := session.authClient.SignUp(email, password) if err != nil { return err } - - srpClient := srp.NewSRPClient(srp.KnownGroups[group], GenerateEncryptedKey(password, lowerCaseEmail, salt), nil) - verifierKey, err := srpClient.Verifier() - if err != nil { - return err - } - signUpRequestBody := &protos.SignupRequest{ - Email: lowerCaseEmail, - Salt: salt, - Verifier: verifierKey.Bytes(), - SkipEmailConfirmation: true, - } - log.Debugf("Sign up request email %v, salt %v verifier %v verifiter in bytes %v", lowerCaseEmail, salt, verifierKey, verifierKey.Bytes()) - signupResponse, err := session.authClient.SignUp(context.Background(), signUpRequestBody) - if err != nil { - return err - } - log.Debugf("sign up response %v", signupResponse) //Request successfull then save salt err = pathdb.Mutate(session.db, func(tx pathdb.TX) error { return pathdb.PutAll(tx, map[string]interface{}{ @@ -1286,69 +1252,12 @@ func signupEmailConfirmation(session *SessionModel, email string, code string) e // Todo find way to optimize this method func login(session *SessionModel, email string, password string) error { - lowerCaseEmail := strings.ToLower(email) start := time.Now() - // Get the salt - salt, err := getUserSalt(session, lowerCaseEmail) - if err != nil { - return err - } - - encryptedKey := GenerateEncryptedKey(password, lowerCaseEmail, salt) - log.Debugf("Encrypted key %v Login", encryptedKey) - // Prepare login request body - client := srp.NewSRPClient(srp.KnownGroups[group], encryptedKey, nil) - //Send this key to client - A := client.EphemeralPublic() - //Create body - prepareRequestBody := &protos.PrepareRequest{ - Email: lowerCaseEmail, - A: A.Bytes(), - } - srpB, err := session.authClient.LoginPrepare(context.Background(), prepareRequestBody) - if err != nil { - return err - } - log.Debugf("Login prepare response %v", srpB) - - // // Once the client receives B from the server Client should check error status here as defense against - // // a malicious B sent from server - B := big.NewInt(0).SetBytes(srpB.B) - - if err = client.SetOthersPublic(B); err != nil { - log.Errorf("Error while setting srpB %v", err) - return err - } - - // client can now make the session key - clientKey, err := client.Key() - if err != nil || clientKey == nil { - return log.Errorf("user_not_found error while generating Client key %v", err) - } - - // // Step 3 - - // // check if the server proof is valid - if !client.GoodServerProof(salt, lowerCaseEmail, srpB.Proof) { - return log.Errorf("user_not_found error while checking server proof%v", err) - } - - clientProof, err := client.ClientProof() - if err != nil { - return log.Errorf("user_not_found error while generating client proof %v", err) - } deviceId, err := session.GetDeviceID() if err != nil { return err } - loginRequestBody := &protos.LoginRequest{ - Email: lowerCaseEmail, - Proof: clientProof, - DeviceId: deviceId, - } - log.Debugf("Login request body %v", loginRequestBody) - - login, err := session.authClient.Login(context.Background(), loginRequestBody) + login, salt, err := session.authClient.Login(email, password, deviceId) if err != nil { return err } diff --git a/internalsdk/webclient/defaultwebclient/rest.go b/internalsdk/webclient/defaultwebclient/rest.go index 24c21a4ac..9fda0c46d 100644 --- a/internalsdk/webclient/defaultwebclient/rest.go +++ b/internalsdk/webclient/defaultwebclient/rest.go @@ -8,6 +8,7 @@ import ( "github.com/getlantern/errors" "github.com/getlantern/golog" "github.com/getlantern/lantern-client/internalsdk/webclient" + "github.com/moul/http2curl" "github.com/go-resty/resty/v2" ) @@ -55,8 +56,8 @@ func SendToURL(httpClient *http.Client, baseURL string, beforeRequest resty.PreR return nil, err } - // command, _ := http2curl.GetCurlCommand(req.RawRequest) - // log.Debugf("curl command: %v", command) + command, _ := http2curl.GetCurlCommand(req.RawRequest) + log.Debugf("curl command: %v", command) responseBody := resp.Body() log.Debugf("response body: %v status code %v", string(responseBody), resp.StatusCode()) diff --git a/lib/account/account.dart b/lib/account/account.dart index 4b8226fa0..f2c3915c5 100644 --- a/lib/account/account.dart +++ b/lib/account/account.dart @@ -58,17 +58,16 @@ class _AccountMenuState extends State { void onAccountManagementTap( BuildContext context, bool isProUser, bool hasUserLoggedIn) { - if (Platform.isIOS) { - if (hasUserLoggedIn) { - // User has gone through onboarding - context.pushRoute(AccountManagement(isPro: isProUser)); - } else { - // Ask user to update their email and password - showProUserDialog(context); - } - } else { + if(Platform.isAndroid){ context.pushRoute(AccountManagement(isPro: isProUser)); } + if (hasUserLoggedIn) { + // User has gone through onboarding + context.pushRoute(AccountManagement(isPro: isProUser)); + } else { + // Ask user to update their email and password + showProUserDialog(context); + } } void openSignIn(BuildContext context) => context.pushRoute(SignIn()); @@ -90,7 +89,6 @@ class _AccountMenuState extends State { List freeItems(BuildContext context, bool hasUserLoggedIn) { return [ - if(Platform.isIOS) if (!hasUserLoggedIn) ListItemFactory.settingsItem( icon: ImagePaths.signIn, @@ -213,13 +211,12 @@ class _AccountMenuState extends State { openSettings(context); }, ), - if (Platform.isIOS) - if (hasUserLoggedIn) - ListItemFactory.settingsItem( - icon: ImagePaths.signOut, - content: 'sign_out'.i18n, - onTap: () => showSingOutDialog(context), - ) + if (hasUserLoggedIn) + ListItemFactory.settingsItem( + icon: ImagePaths.signOut, + content: 'sign_out'.i18n, + onTap: () => showSingOutDialog(context), + ) ]; } @@ -230,21 +227,13 @@ class _AccountMenuState extends State { automaticallyImplyLeading: false, body: sessionModel .proUser((BuildContext sessionContext, bool proUser, Widget? child) { - if (Platform.isIOS) { - return sessionModel.isUserSignedIn((context, hasUserLoggedIn, child) { - return ListView( - children: proUser - ? proItems(sessionContext, hasUserLoggedIn) - : freeItems(sessionContext, hasUserLoggedIn), - ); - }); - } - - return ListView( - children: proUser - ? proItems(sessionContext, false) - : freeItems(sessionContext, false), - ); + return sessionModel.isUserSignedIn((context, hasUserLoggedIn, child) { + return ListView( + children: proUser + ? proItems(sessionContext, hasUserLoggedIn) + : freeItems(sessionContext, hasUserLoggedIn), + ); + }); }), ); } diff --git a/lib/account/account_management.dart b/lib/account/account_management.dart index 6ce3bcd86..fb69554cf 100644 --- a/lib/account/account_management.dart +++ b/lib/account/account_management.dart @@ -309,11 +309,11 @@ class _AccountManagementState extends State header: 'lantern_pro_email'.i18n, icon: ImagePaths.email, content: emailAddress, - trailingArray: [ - ], + trailingArray: [], ); }), - if(Platform.isIOS) + + if(!Platform.isAndroid) ListItemFactory.settingsItem( header: 'password'.i18n, icon: ImagePaths.lockFiled, @@ -339,7 +339,7 @@ class _AccountManagementState extends State }), //Disable device linking in IOS const UserDevices(), - if(Platform.isIOS) + if(!Platform.isAndroid) ListItemFactory.settingsItem( header: 'danger_zone'.i18n, icon: ImagePaths.alert, @@ -428,6 +428,7 @@ class UserDevices extends StatelessWidget { return Column(children: [ ...devices.devices.map((device) { var isMyDevice = device.id == myDeviceId; + print('Device: ${device.id} isMyDevice: $myDeviceId'); var allowRemoval = devices.devices.length > 1 || !isMyDevice; var index = devices.devices.indexWhere((d) => d == device); return Padding( diff --git a/lib/account/auth/create_account_email.dart b/lib/account/auth/create_account_email.dart index 5a531a3e0..c5b0dc120 100644 --- a/lib/account/auth/create_account_email.dart +++ b/lib/account/auth/create_account_email.dart @@ -107,37 +107,6 @@ class _CreateAccountEmailState extends State { createAccount(); } - void _showEmailVerificationDialog({required VoidCallback onVerified}) { - CDialog( - title: "check_your_email".i18n, - description: Column( - mainAxisSize: MainAxisSize.min, - children: [ - CText( - "please_verify_email".i18n, - style: - tsBody1.copiedWith(fontWeight: FontWeight.w400, color: grey5), - ), - const SizedBox(height: 24.0), - EmailTag(email: _emailController.text.validateEmail), - const SizedBox(height: 24.0), - ], - ), - barrierDismissible: false, - dismissText: 'change_email'.i18n.toUpperCase(), - agreeText: "verify".i18n.toUpperCase(), - agreeAction: () async { - context.popRoute(); - Future.delayed( - const Duration(milliseconds: 300), - () { - onVerified.call(); - }, - ); - return false; - }, - ).show(context); - } /// Process for creating account /// Create new temp account with random password diff --git a/lib/account/auth/verification.dart b/lib/account/auth/verification.dart index 0d18ef881..4211e0b48 100644 --- a/lib/account/auth/verification.dart +++ b/lib/account/auth/verification.dart @@ -115,7 +115,8 @@ class _VerificationState extends State { case AuthFlow.changeEmail: resendChangeEmailVerificationCode(); case AuthFlow.signIn: - /// there is no verification flow for sign in + + /// there is no verification flow for sign in case AuthFlow.updateAccount: resendResetEmailVerificationCode(); } @@ -156,7 +157,8 @@ class _VerificationState extends State { _verifyEmail(code); break; case AuthFlow.reset: - openResetPassword(code); + _verifyEmail(code); + break; case AuthFlow.signIn: @@ -175,7 +177,8 @@ class _VerificationState extends State { } void openResetPassword(String code) { - context.pushRoute(ResetPassword(email: widget.email, code: code,authFlow: widget.authFlow)); + context.pushRoute(ResetPassword( + email: widget.email, code: code, authFlow: widget.authFlow)); } void _verifyEmail(String code) async { @@ -228,6 +231,16 @@ class _VerificationState extends State { // Purchase flow void startPurchase() { + switch (Platform.operatingSystem) { + case "ios": + _proceedToCheckoutIOS(); + break; + default: + _proceedToCheckout(); + } + } + + void _proceedToCheckoutIOS() { assert(widget.plan != null, 'Plan object is null'); final appPurchase = sl(); try { @@ -260,6 +273,16 @@ class _VerificationState extends State { } } + void _proceedToCheckout() { + context.pushRoute(Checkout( + plan: widget.plan!, + isPro: false, + authFlow: widget.authFlow, + email: widget.email, + verificationPin: pinCodeController.text, + )); + } + void openPassword() { context.pushRoute(CreateAccountPassword( email: widget.email.validateEmail, @@ -272,7 +295,7 @@ class _VerificationState extends State { case AuthFlow.signIn: // TODO: Handle this case. case AuthFlow.reset: - context.router.maybePop(); + openResetPassword(code); case AuthFlow.createAccount: startPurchase(); case AuthFlow.verifyEmail: @@ -292,7 +315,8 @@ class _VerificationState extends State { Future onBackPressed() async { if (widget.authFlow == AuthFlow.createAccount || - widget.authFlow == AuthFlow.updateAccount||widget.authFlow == AuthFlow.proCodeActivation) { + widget.authFlow == AuthFlow.updateAccount || + widget.authFlow == AuthFlow.proCodeActivation) { assert(widget.tempPassword != null, 'Temp password is null'); // if user press back button while creating account // we need to delete that temp account diff --git a/lib/account/report_issue.dart b/lib/account/report_issue.dart index c6a1602bc..67c7c666a 100644 --- a/lib/account/report_issue.dart +++ b/lib/account/report_issue.dart @@ -211,6 +211,7 @@ class _ReportIssueState extends State { }, ); } catch (error, stackTrace) { + print(stackTrace); AppLoadingDialog.dismissLoadingDialog(context); CDialog.showError( context, diff --git a/lib/app.dart b/lib/app.dart index 90f2608ae..857d10ddc 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -192,6 +192,9 @@ class _LanternAppState extends State } DeepLink navigateToDeepLink(PlatformDeepLink deepLink) { + if(!Platform.isAndroid){ + return DeepLink.defaultPath; + } logger.d("DeepLink configuration: ${deepLink.configuration.toString()}"); if (deepLink.path.toLowerCase().startsWith('/report-issue')) { logger.d("DeepLink uri: ${deepLink.uri.toString()}"); diff --git a/lib/common/session_model.dart b/lib/common/session_model.dart index 791c58383..1a4673108 100644 --- a/lib/common/session_model.dart +++ b/lib/common/session_model.dart @@ -3,7 +3,6 @@ import 'package:intl/intl.dart'; import 'package:lantern/custom_bottom_bar.dart'; import 'package:lantern/plans/utils.dart'; import 'package:lantern/replica/common.dart'; - import 'common.dart'; import 'common_desktop.dart'; @@ -74,10 +73,11 @@ class SessionModel extends Model { 'hasSucceedingProxy', false, ); - userEmail = ffiValueNotifier(ffiEmailAddress, 'emailAddress',""); - proUserNotifier = ffiValueNotifier(ffiProUser,'prouser', false); + userEmail = ffiValueNotifier(ffiEmailAddress, 'emailAddress', ""); + proUserNotifier = ffiValueNotifier(ffiProUser, 'prouser', false); + hasUserSignedInNotifier = + ffiValueNotifier(ffiIsUserLoggedIn, 'IsUserLoggedIn', false); } - if (Platform.isAndroid) { // By default when user starts the app we need to make sure that screenshot is disabled // if user goes to chat then screenshot will be disabled @@ -89,25 +89,6 @@ class SessionModel extends Model { return singleValueNotifier(path, defaultValue); } - // listenWebsocket listens for websocket messages from the server. If a message matches the given message type, - // the onMessage callback is triggered with the given property value - void listenWebsocket(WebsocketImpl? websocket, String messageType, - String? property, void Function(T?) onMessage) { - if (websocket == null) return; - websocket.messageStream.listen( - (json) { - if (json["type"] == messageType) { - if (property != null) { - onMessage(json["message"][property]); - } else { - onMessage(json["message"]); - } - } - }, - onError: (error) => appLogger.i("websocket error: ${error.description}"), - ); - } - Widget proUser(ValueWidgetBuilder builder) { if (isMobile()) { return subscribedSingleValueBuilder('prouser', builder: builder); @@ -116,10 +97,13 @@ class SessionModel extends Model { return ffiValueBuilder( 'prouser', defaultValue: false, - onChanges: (setValue) => - listenWebsocket(websocket, "pro", "userStatus", (value) { - if (value != null && value.toString() == "active") setValue(true); - }), + onChanges: (setValue) => { + listenWebsocket(websocket, 'pro', 'isProUser', (p0) { + if (p0 != null) { + setValue(p0 as bool); + } + }) + }, ffiProUser, builder: builder, ); @@ -270,7 +254,7 @@ class SessionModel extends Model { } return ffiValueBuilder( 'deviceid', - ffiReferral, + ffiDeviceId, defaultValue: '', builder: builder, ); @@ -309,9 +293,31 @@ class SessionModel extends Model { ); } + /// This only supports desktop fo now + Future testProviderRequest( + String email, String paymentProvider, String planId) { + return compute(ffiTestPaymentRequest, [email, paymentProvider, planId]); + } + ///Auth Widgets Widget isUserSignedIn(ValueWidgetBuilder builder) { + final websocket = WebsocketImpl.instance(); + if (isDesktop()) { + return ffiValueBuilder( + 'IsUserLoggedIn', + ffiIsUserLoggedIn, + defaultValue: false, + builder: builder, + onChanges: (setValue) { + listenWebsocket(websocket, 'pro', 'login', (userLoggedIn) { + if (userLoggedIn != null) { + setValue(userLoggedIn as bool); + } + }); + }, + ); + } return subscribedSingleValueBuilder('IsUserLoggedIn', builder: builder, defaultValue: false); } @@ -319,6 +325,9 @@ class SessionModel extends Model { /// Auth Method channel Future signUp(String email, String password) { + if (isDesktop()) { + return compute(ffiSignUp, [email, password]); + } return methodChannel.invokeMethod('signup', { 'email': email, 'password': password, @@ -341,6 +350,9 @@ class SessionModel extends Model { } Future login(String email, String password) { + if (isDesktop()) { + return compute(ffiLogin, [email, password]); + } return methodChannel.invokeMethod('login', { 'email': email, 'password': password, @@ -348,6 +360,9 @@ class SessionModel extends Model { } Future startRecoveryByEmail(String email) { + if (isDesktop()) { + return compute(ffiStartRecoveryByEmail, email); + } return methodChannel.invokeMethod('startRecoveryByEmail', { 'email': email, }); @@ -355,6 +370,9 @@ class SessionModel extends Model { Future completeRecoveryByEmail( String email, String password, String code) { + if (isDesktop()) { + return compute(ffiCompleteRecoveryByEmail, [email, password, code]); + } return methodChannel .invokeMethod('completeRecoveryByEmail', { 'email': email, @@ -364,6 +382,9 @@ class SessionModel extends Model { } Future validateRecoveryCode(String email, String code) { + if (isDesktop()) { + return compute(ffiValidateRecoveryByEmail, [email, code]); + } return methodChannel.invokeMethod('validateRecoveryCode', { 'email': email, 'code': code, @@ -390,23 +411,34 @@ class SessionModel extends Model { } Future signOut() { + if (isDesktop()) { + return compute(ffiLogout, ''); + } return methodChannel.invokeMethod('signOut', {}); } Future deleteAccount(String password) { + if (isDesktop()) { + return compute(ffiDeleteAccount, password); + } return methodChannel.invokeMethod('deleteAccount', { 'password': password, }); } Future isUserFirstTimeVisit() async { + if (isDesktop()) { + return await ffiUserFirstVisit(); + } final firsTime = await methodChannel .invokeMethod('isUserFirstTimeVisit', {}); - print("firsTime $firsTime"); return firsTime ?? false; } Future setFirstTimeVisit() async { + if (isDesktop()) { + return setUserFirstTimeVisit(); + } return methodChannel .invokeMethod('setFirstTimeVisit', {}); } @@ -791,8 +823,9 @@ class SessionModel extends Model { print('value $value'); }); } - ffiRedeemResellerCode(email.toNativeUtf8(), currency.toNativeUtf8(), - deviceName.toNativeUtf8(), resellerCode.toNativeUtf8()); + + await compute( + ffiRedeemResellerCode, [email, currency, deviceName, resellerCode]); } Future submitBitcoinPayment( @@ -928,6 +961,25 @@ class SessionModel extends Model { ); } + // listenWebsocket listens for websocket messages from the server. If a message matches the given message type, + // the onMessage callback is triggered with the given property value + void listenWebsocket(WebsocketImpl? websocket, String messageType, + String? property, void Function(T?) onMessage) { + if (websocket == null) return; + websocket.messageStream.listen( + (json) { + if (json["type"] == messageType) { + if (property != null) { + onMessage(json["message"][property]); + } else { + onMessage(json["message"]); + } + } + }, + onError: (error) => appLogger.i("websocket error: ${error.description}"), + ); + } + Future setSplitTunneling(bool on) async { unawaited( methodChannel.invokeMethod('setSplitTunneling', { diff --git a/lib/core/router/router.gr.dart b/lib/core/router/router.gr.dart index 8fdbcb2ed..3f4f1c006 100644 --- a/lib/core/router/router.gr.dart +++ b/lib/core/router/router.gr.dart @@ -193,6 +193,9 @@ abstract class $AppRouter extends _i53.RootStackRouter { child: _i15.Checkout( plan: args.plan, isPro: args.isPro, + authFlow: args.authFlow, + email: args.email, + verificationPin: args.verificationPin, key: args.key, ), ); @@ -896,6 +899,9 @@ class Checkout extends _i53.PageRouteInfo { Checkout({ required _i55.Plan plan, required bool isPro, + _i55.AuthFlow? authFlow, + String? email, + String? verificationPin, _i55.Key? key, List<_i53.PageRouteInfo>? children, }) : super( @@ -903,6 +909,9 @@ class Checkout extends _i53.PageRouteInfo { args: CheckoutArgs( plan: plan, isPro: isPro, + authFlow: authFlow, + email: email, + verificationPin: verificationPin, key: key, ), initialChildren: children, @@ -918,6 +927,9 @@ class CheckoutArgs { const CheckoutArgs({ required this.plan, required this.isPro, + this.authFlow, + this.email, + this.verificationPin, this.key, }); @@ -925,11 +937,17 @@ class CheckoutArgs { final bool isPro; + final _i55.AuthFlow? authFlow; + + final String? email; + + final String? verificationPin; + final _i55.Key? key; @override String toString() { - return 'CheckoutArgs{plan: $plan, isPro: $isPro, key: $key}'; + return 'CheckoutArgs{plan: $plan, isPro: $isPro, authFlow: $authFlow, email: $email, verificationPin: $verificationPin, key: $key}'; } } diff --git a/lib/ffi.dart b/lib/ffi.dart index 624f56437..8fea83e1f 100644 --- a/lib/ffi.dart +++ b/lib/ffi.dart @@ -7,7 +7,7 @@ import 'generated_bindings.dart'; extension StringEx on String { Pointer toPointerChar() { - return this.toNativeUtf8().cast(); + return toNativeUtf8().cast(); } bool toBool() { @@ -15,6 +15,29 @@ extension StringEx on String { } } +const String _libName = 'liblantern'; + +final DynamicLibrary _dylib = () { + if (Platform.isMacOS) { + return DynamicLibrary.open('$_libName.dylib'); + } + if (Platform.isLinux) { + String dir = Directory.current.path; + return DynamicLibrary.open('$dir/$_libName.so'); + } + if (Platform.isWindows) { + return DynamicLibrary.open('$_libName.dll'); + } + throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}'); +}(); + +/// The bindings to the native functions in [dylib]. +final NativeLibrary _bindings = NativeLibrary(_dylib); + +void loadLibrary() { + _bindings.start(); +} + void sysProxyOn() => _bindings.sysProxyOn(); void sysProxyOff() => _bindings.sysProxyOff(); @@ -62,18 +85,6 @@ Future ffiUserData() async { return (proxy.toBool(), config.toBool(), success.toBool()); } -// checkAPIError throws a PlatformException if the API response contains an error -void checkAPIError(result, errorMessage) { - if (result is String) { - final errorMessageMap = jsonDecode(result); - throw PlatformException( - code: errorMessageMap.toString(), message: errorMessage); - } - if (result.error != "") { - throw PlatformException(code: result.error, message: errorMessage); - } -} - Future ffiApproveDevice(String code) async { final json = await _bindings .approveDevice(code.toPointerChar()) @@ -117,18 +128,23 @@ Future ffiEmailExists(String email) async => await _bindings .cast() .toDartString(); -void ffiRedeemResellerCode(email, currency, deviceName, resellerCode) { +Future ffiRedeemResellerCode(List params) { + final email = params[0].toPointerChar(); + final currency = params[1].toPointerChar(); + final deviceName = params[2].toPointerChar(); + final resellerCode = params[3].toPointerChar(); final result = _bindings .redeemResellerCode(email, currency, deviceName, resellerCode) .cast() .toDartString(); checkAPIError(result, 'wrong_seller_code'.i18n); - // if successful redeeming a reseller code, immediately refresh Pro user data - ffiProUser(); + return Future.value(); } Pointer ffiReferral() => _bindings.referral().cast(); +Pointer ffiDeviceId() => _bindings.myDeviceId().cast(); + Pointer ffiReplicaAddr() => _bindings.replicaAddr().cast(); Pointer ffiChatEnabled() => _bindings.chatEnabled().cast(); @@ -159,17 +175,15 @@ Pointer ffiOnBoardingStatus() => Pointer ffiServerInfo() => _bindings.serverInfo().cast(); Future ffiReportIssue(List list) { - final email = list[0].toNativeUtf8(); - final issueType = list[1].toNativeUtf8(); - final description = list[2].toNativeUtf8(); - final result = _bindings.reportIssue(email as Pointer, - issueType as Pointer, description as Pointer); - if (result.r1 != nullptr) { - // Got error throw error to show error ui state - final errorCode = result.r1.cast().toDartString(); - throw PlatformException( - code: errorCode, message: 'report_issue_error'.i18n); - } + final email = list[0].toPointerChar(); + final issueType = list[1].toPointerChar(); + final description = list[2].toPointerChar(); + final result = _bindings + .reportIssue(email, issueType, description) + .cast() + .toDartString(); + + checkAPIError(result, 'we_are_experiencing_technical_difficulties'.i18n); return Future.value(); } @@ -189,33 +203,139 @@ Future ffiPaymentRedirect(List list) { return Future.value(result.redirect); } -const String _libName = 'liblantern'; +Future ffiTestPaymentRequest(List params) { + final email = params[0].toPointerChar(); + final paymentProvider = params[1].toPointerChar(); + final planId = params[2].toPointerChar(); + final result = _bindings + .testProviderRequest(email, paymentProvider, planId) + .cast() + .toDartString(); + checkAuthAPIError(result); + return Future.value(); +} -final DynamicLibrary _dylib = () { - if (Platform.isMacOS) { - return DynamicLibrary.open('$_libName.dylib'); - } - if (Platform.isLinux) { - String dir = Directory.current.path; - return DynamicLibrary.open('$dir/$_libName.so'); - } - if (Platform.isWindows) { - return DynamicLibrary.open('$_libName.dll'); - } - throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}'); -}(); +/// Auth methods for desktop -/// The bindings to the native functions in [dylib]. -final NativeLibrary _bindings = NativeLibrary(_dylib); +/// FFI pointer to the native function +Pointer ffiIsUserLoggedIn() { + final result = _bindings.isUserLoggedIn().cast(); + print(result.toDartString()); + return result; +} -void loadLibrary() { - _bindings.start(); +/// FFI function +Future ffiUserFirstVisit() { + final result = _bindings.isUserFirstTime().cast().toDartString(); + return Future.value(result == 'true'); } -//Custom exception for handling error +void setUserFirstTimeVisit() => _bindings.setFirstTimeVisit(); + +///signup +Future ffiSignUp(List params) { + final email = params[0].toPointerChar(); + final password = params[1].toPointerChar(); + final result = _bindings.signup(email, password).cast().toDartString(); + checkAuthAPIError(result); + return Future.value(result.toBool()); +} + +/// login +Future ffiLogin(List params) { + final email = params[0].toPointerChar(); + final password = params[1].toPointerChar(); + final result = _bindings.login(email, password).cast().toDartString(); + checkAuthAPIError(result); + return Future.value(result.toBool()); +} + +/// logout +Future ffiLogout(dynamic context) { + final result = _bindings.logout().cast().toDartString(); + checkAuthAPIError(result); + return Future.value(result.toBool()); +} + +/// start recovery by email +/// send verification code to email +Future ffiStartRecoveryByEmail(String email) { + final result = _bindings + .startRecoveryByEmail(email.toPointerChar()) + .cast() + .toDartString(); + checkAuthAPIError(result); + return Future.value(result.toBool()); +} + +/// start recovery by email +/// send verification code to email +Future ffiValidateRecoveryByEmail(List params) { + final email = params[0].toPointerChar(); + final code = params[1].toPointerChar(); + final result = _bindings + .validateRecoveryByEmail(email, code) + .cast() + .toDartString(); + checkAuthAPIError(result); + return Future.value(result.toBool()); +} + +Future ffiCompleteRecoveryByEmail(List params) { + final email = params[0].toPointerChar(); + final password = params[1].toPointerChar(); + final code = params[2].toPointerChar(); + final result = _bindings + .completeRecoveryByEmail(email, code, password) + .cast() + .toDartString(); + checkAuthAPIError(result); + return Future.value(result.toBool()); +} + +Future ffiDeleteAccount(String password) { + final result = _bindings + .deleteAccount(password.toPointerChar()) + .cast() + .toDartString(); + checkAuthAPIError(result); + return Future.value(result.toBool()); +} +//Custom exception for handling error class NoPlansUpdate implements Exception { String message; NoPlansUpdate(this.message); } + +// checkAPIError throws a PlatformException if the API response contains an error +void checkAPIError(result, errorMessage) { + if (result is String) { + if (result == 'true') { + return; + } + final errorMessageMap = jsonDecode(result); + if (errorMessageMap.containsKey('error')) { + throw PlatformException( + code: errorMessageMap['error'], message: errorMessage); + } + return; + } + if (result.error != "") { + throw PlatformException(code: result.error, message: errorMessage); + } +} + +void checkAuthAPIError(result) { + if (result is String) { + if (result == "true") { + return; + } + final errorMessageMap = jsonDecode(result); + if (errorMessageMap.containsKey('error')) { + throw PlatformException( + code: errorMessageMap['error'], message: errorMessageMap['error']); + } + } +} diff --git a/lib/generated_bindings.dart b/lib/generated_bindings.dart index 4410e4dd4..b59003903 100644 --- a/lib/generated_bindings.dart +++ b/lib/generated_bindings.dart @@ -245,6 +245,28 @@ class NativeLibrary { late final _emailExists = _emailExistsPtr .asFunction Function(ffi.Pointer)>(); + ffi.Pointer testProviderRequest( + ffi.Pointer email, + ffi.Pointer paymentProvider, + ffi.Pointer plan, + ) { + return _testProviderRequest( + email, + paymentProvider, + plan, + ); + } + + late final _testProviderRequestPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('testProviderRequest'); + late final _testProviderRequest = _testProviderRequestPtr.asFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + /// The function returns two C strings: the first represents success, and the second represents an error. /// If the redemption is successful, the first string contains "true", and the second string is nil. /// If an error occurs during redemption, the first string is nil, and the second string contains the error message. @@ -285,6 +307,16 @@ class NativeLibrary { late final _referral = _referralPtr.asFunction Function()>(); + ffi.Pointer myDeviceId() { + return _myDeviceId(); + } + + late final _myDeviceIdPtr = + _lookup Function()>>( + 'myDeviceId'); + late final _myDeviceId = + _myDeviceIdPtr.asFunction Function()>(); + ffi.Pointer chatEnabled() { return _chatEnabled(); } @@ -494,7 +526,7 @@ class NativeLibrary { late final _replicaAddr = _replicaAddrPtr.asFunction Function()>(); - reportIssue_return reportIssue( + ffi.Pointer reportIssue( ffi.Pointer email, ffi.Pointer issueType, ffi.Pointer description, @@ -508,11 +540,11 @@ class NativeLibrary { late final _reportIssuePtr = _lookup< ffi.NativeFunction< - reportIssue_return Function(ffi.Pointer, + ffi.Pointer Function(ffi.Pointer, ffi.Pointer, ffi.Pointer)>>('reportIssue'); late final _reportIssue = _reportIssuePtr.asFunction< - reportIssue_return Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer)>(); + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); ffi.Pointer checkUpdates() { return _checkUpdates(); @@ -523,6 +555,154 @@ class NativeLibrary { 'checkUpdates'); late final _checkUpdates = _checkUpdatesPtr.asFunction Function()>(); + + ffi.Pointer isUserFirstTime() { + return _isUserFirstTime(); + } + + late final _isUserFirstTimePtr = + _lookup Function()>>( + 'isUserFirstTime'); + late final _isUserFirstTime = + _isUserFirstTimePtr.asFunction Function()>(); + + void setFirstTimeVisit() { + return _setFirstTimeVisit(); + } + + late final _setFirstTimeVisitPtr = + _lookup>('setFirstTimeVisit'); + late final _setFirstTimeVisit = + _setFirstTimeVisitPtr.asFunction(); + + ffi.Pointer isUserLoggedIn() { + return _isUserLoggedIn(); + } + + late final _isUserLoggedInPtr = + _lookup Function()>>( + 'isUserLoggedIn'); + late final _isUserLoggedIn = + _isUserLoggedInPtr.asFunction Function()>(); + + ffi.Pointer signup( + ffi.Pointer email, + ffi.Pointer password, + ) { + return _signup( + email, + password, + ); + } + + late final _signupPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>>('signup'); + late final _signup = _signupPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>(); + + ffi.Pointer login( + ffi.Pointer email, + ffi.Pointer password, + ) { + return _login( + email, + password, + ); + } + + late final _loginPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>>('login'); + late final _login = _loginPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>(); + + ffi.Pointer logout() { + return _logout(); + } + + late final _logoutPtr = + _lookup Function()>>('logout'); + late final _logout = + _logoutPtr.asFunction Function()>(); + + /// Send recovery code to user email + ffi.Pointer startRecoveryByEmail( + ffi.Pointer email, + ) { + return _startRecoveryByEmail( + email, + ); + } + + late final _startRecoveryByEmailPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('startRecoveryByEmail'); + late final _startRecoveryByEmail = _startRecoveryByEmailPtr + .asFunction Function(ffi.Pointer)>(); + + /// Complete recovery by email + ffi.Pointer completeRecoveryByEmail( + ffi.Pointer email, + ffi.Pointer code, + ffi.Pointer password, + ) { + return _completeRecoveryByEmail( + email, + code, + password, + ); + } + + late final _completeRecoveryByEmailPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('completeRecoveryByEmail'); + late final _completeRecoveryByEmail = _completeRecoveryByEmailPtr.asFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + + /// // This will validate code send by server + ffi.Pointer validateRecoveryByEmail( + ffi.Pointer email, + ffi.Pointer code, + ) { + return _validateRecoveryByEmail( + email, + code, + ); + } + + late final _validateRecoveryByEmailPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer)>>('validateRecoveryByEmail'); + late final _validateRecoveryByEmail = _validateRecoveryByEmailPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>(); + + /// This will delete user accoutn and creates new user + ffi.Pointer deleteAccount( + ffi.Pointer password, + ) { + return _deleteAccount( + password, + ); + } + + late final _deleteAccountPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('deleteAccount'); + late final _deleteAccount = _deleteAccountPtr + .asFunction Function(ffi.Pointer)>(); } /// mbstate_t is an opaque object to keep conversion state, during multibyte @@ -653,13 +833,6 @@ typedef GoInt = GoInt64; typedef GoInt64 = ffi.LongLong; typedef DartGoInt64 = int; -/// Return type for reportIssue -final class reportIssue_return extends ffi.Struct { - external ffi.Pointer r0; - - external ffi.Pointer r1; -} - const int __has_safe_buffers = 1; const int __DARWIN_ONLY_64_BIT_INO_T = 1; diff --git a/lib/home.dart b/lib/home.dart index 72b4a75b7..712c35c2f 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -104,8 +104,6 @@ class _HomePageState extends State with WindowListener { } Future _checkForFirstTimeVisit() async { - if (!Platform.isIOS) return; - checkForFirstTimeVisit() async { if (sessionModel.proUserNotifier.value == null) { return; @@ -215,9 +213,9 @@ class _HomePageState extends State with WindowListener { // not already been accepted return const PrivacyDisclosure(); } - if (Platform.isIOS) { + + if(!Platform.isAndroid){ userNew(() { - print("called user new function"); _checkForFirstTimeVisit(); }); } diff --git a/lib/plans/checkout.dart b/lib/plans/checkout.dart index 655a56584..d2138969a 100644 --- a/lib/plans/checkout.dart +++ b/lib/plans/checkout.dart @@ -1,8 +1,6 @@ -import 'package:email_validator/email_validator.dart'; import 'package:lantern/common/common.dart'; import 'package:lantern/common/common_desktop.dart'; import 'package:lantern/plans/payment_provider.dart'; -import 'package:lantern/plans/plan_details.dart'; import 'package:lantern/plans/utils.dart'; import 'package:retry/retry.dart'; @@ -10,10 +8,16 @@ import 'package:retry/retry.dart'; class Checkout extends StatefulWidget { final Plan plan; final bool isPro; + final AuthFlow? authFlow; + final String? verificationPin; + final String? email; const Checkout({ required this.plan, required this.isPro, + this.authFlow, + this.email, + this.verificationPin, Key? key, }) : super(key: key); @@ -25,15 +29,6 @@ class _CheckoutState extends State with SingleTickerProviderStateMixin { bool showMoreOptions = false; bool showContinueButton = false; - final emailFieldKey = GlobalKey(); - late final emailController = CustomTextEditingController( - formKey: emailFieldKey, - validator: (value) => value!.isEmpty - ? null - : EmailValidator.validate(value ?? '') - ? null - : 'please_enter_a_valid_email_address'.i18n, - ); final refCodeFieldKey = GlobalKey(); late final refCodeController = CustomTextEditingController( @@ -79,6 +74,8 @@ class _CheckoutState extends State return BaseScreen( resizeToAvoidBottomInset: false, title: 'lantern_pro_checkout'.i18n, + padHorizontal: true, + padVertical: true, body: sessionModel.paymentMethods( builder: ( context, @@ -86,118 +83,71 @@ class _CheckoutState extends State Widget? child, ) { defaultProviderIfNecessary(paymentMethods.toList()); - return sessionModel.emailAddress(( - BuildContext context, - String emailAddress, - Widget? child, - ) { - return Container( - padding: const EdgeInsetsDirectional.only( - start: 16, - end: 16, - top: 24, - bottom: 32, + return Column( + children: [ + CText('choose_payment_method'.i18n, style: tsHeading1), + const SizedBox(height: 24), + Container( + padding: + const EdgeInsetsDirectional.only(top: 16, bottom: 16), + width: MediaQuery.of(context).size.width, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: paymentOptions(paymentMethods)), ), - child: Column( - children: [ - // * Step 2 - PlanStep( - stepNum: '2', - description: 'enter_email'.i18n, - ), - // * Email field - Container( - padding: const EdgeInsetsDirectional.only( - top: 8, - bottom: 8, - ), - child: Form( - key: emailFieldKey, - child: CTextField( - initialValue: widget.isPro ? emailAddress : '', - controller: emailController, - onChanged: (text) { - setState(() { - showContinueButton = enableContinueButton(); - }); - }, - autovalidateMode: widget.isPro - ? AutovalidateMode.always - : AutovalidateMode.disabled, - label: 'email'.i18n, - keyboardType: TextInputType.emailAddress, - prefixIcon: const CAssetImage(path: ImagePaths.email), - ), - ), + if (isRefCodeFieldShowing) + Form( + key: refCodeFieldKey, + child: CTextField( + controller: refCodeController, + autovalidateMode: AutovalidateMode.disabled, + onChanged: (text) { + setState(() { + showContinueButton = enableContinueButton(); + }); + }, + textCapitalization: TextCapitalization.characters, + label: 'referral_code'.i18n, + keyboardType: TextInputType.text, + prefixIcon: const CAssetImage(path: ImagePaths.star), ), - if (isRefCodeFieldShowing) - Form( - key: refCodeFieldKey, - child: CTextField( - controller: refCodeController, - autovalidateMode: AutovalidateMode.disabled, - onChanged: (text) { - setState(() { - showContinueButton = enableContinueButton(); - }); - }, - textCapitalization: TextCapitalization.characters, - label: 'referral_code'.i18n, - keyboardType: TextInputType.text, - prefixIcon: const CAssetImage(path: ImagePaths.star), + ) + else + GestureDetector( + onTap: () { + setState(() { + isRefCodeFieldShowing = true; + }); + }, + child: Row( + children: [ + const CAssetImage(path: ImagePaths.add), + Padding( + padding: const EdgeInsetsDirectional.only( + start: 8.0, + ), + child: CText( + 'add_referral_code'.i18n, + style: tsBody1, + ), ), - ) - else - GestureDetector( - onTap: () { - setState(() { - isRefCodeFieldShowing = true; - }); - }, - child: Row( - children: [ - const CAssetImage(path: ImagePaths.add), - Padding( - padding: const EdgeInsetsDirectional.only( - start: 8.0, - ), - child: CText( - 'add_referral_code'.i18n, - style: tsBody1, - ), - ), - ], - ), - ), - - const SizedBox(height: 16.0), - PlanStep( - stepNum: '3', - description: 'choose_payment_method'.i18n, - ), - //* Payment options - Container( - padding: - const EdgeInsetsDirectional.only(top: 16, bottom: 16), - width: MediaQuery.of(context).size.width, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: paymentOptions(paymentMethods)), + ], ), - // * Price summary, unused pro time disclaimer, Continue button - - Tooltip( - message: AppKeys.continueCheckout, - child: Button( - text: 'continue'.i18n, - disabled: !enableContinueButton(), - onPressed: onContinueTapped, - ), + ), + const Spacer(), + Tooltip( + message: AppKeys.continueCheckout, + child: SizedBox( + width: double.infinity, + child: Button( + text: 'continue'.i18n, + disabled: !enableContinueButton(), + onPressed: onContinueTapped, ), - ], + ), ), - ); - }); + ], + ); }, )); } @@ -260,15 +210,10 @@ class _CheckoutState extends State } bool enableContinueButton() { - if (emailFieldKey.currentState == null) { - return false; - } - final isEmailValid = emailController.value.text.isNotEmpty && - emailFieldKey.currentState!.validate(); if (!isRefCodeFieldShowing || refCodeController.text.isEmpty) { - return isEmailValid; + return true; } - return isEmailValid && refCodeFieldKey.currentState!.validate(); + return refCodeFieldKey.currentState!.validate(); } //Class methods @@ -286,7 +231,7 @@ class _CheckoutState extends State } } - Future resolvePaymentRoute() async { + Future resolvePaymentMethod() async { switch (selectedPaymentProvider!) { case Providers.stripe: if (isDesktop()) { @@ -325,13 +270,18 @@ class _CheckoutState extends State } _proceedWithPaymentWall(); break; + case Providers.test: + if (isDesktop()) { + _proceedTestRequest(); + return; + } } } Future _proceedWithStripe() async { await context.pushRoute( StripeCheckout( - email: emailController.text, + email: widget.email!, refCode: refCodeController.text, plan: widget.plan, isPro: widget.isPro, @@ -344,7 +294,7 @@ class _CheckoutState extends State context.loaderOverlay.show(); final value = await sessionModel.generatePaymentRedirectUrl( planID: widget.plan.id, - email: emailController.text, + email: widget.email!, paymentProvider: Providers.btcpay); context.loaderOverlay.hide(); @@ -362,7 +312,7 @@ class _CheckoutState extends State final value = await sessionModel.generatePaymentRedirectUrl( planID: widget.plan.id, - email: emailController.text, + email: widget.email!, paymentProvider: Providers.fropay); context.loaderOverlay.hide(); @@ -374,13 +324,30 @@ class _CheckoutState extends State } } + void _proceedTestRequest() async { + try { + context.loaderOverlay.show(); + final value = await sessionModel.testProviderRequest( + widget.email!, Providers.test.name, widget.plan.id); + context.loaderOverlay.hide(); + if (widget.isPro) { + showSuccessDialog(context, widget.isPro); + } else { + resolveRoute(); + } + } catch (error, stackTrace) { + context.loaderOverlay.hide(); + showError(context, error: error, stackTrace: stackTrace); + } + } + void _proceedWithShepherd() async { try { context.loaderOverlay.show(); final value = await sessionModel.generatePaymentRedirectUrl( planID: widget.plan.id, - email: emailController.text, + email: widget.email!, paymentProvider: Providers.shepherd); context.loaderOverlay.hide(); @@ -421,7 +388,7 @@ class _CheckoutState extends State final redirectUrl = await sessionModel.paymentRedirectForDesktop( context, widget.plan.id, - emailController.text, + widget.email!, provider, ); context.loaderOverlay.hide(); @@ -443,7 +410,7 @@ class _CheckoutState extends State context.loaderOverlay.show(); final value = await sessionModel.generatePaymentRedirectUrl( planID: widget.plan.id, - email: emailController.text, + email: widget.email!, paymentProvider: Providers.paymentwall); context.loaderOverlay.hide(); @@ -464,7 +431,7 @@ class _CheckoutState extends State var currencyCost = widget.plan.price[currency]; if (currencyCost == null) return; await sessionModel.submitFreekassa( - emailController.text, + widget.email!, widget.plan.id, currencyCost.toString(), ); @@ -495,7 +462,7 @@ class _CheckoutState extends State if (refCode.text.isNotEmpty) { await sessionModel.applyRefCode(refCode.text); } - resolvePaymentRoute(); + resolvePaymentMethod(); } catch (e) { if (refCode.text.isNotEmpty) { refCodeController.error = 'invalid_or_incomplete_referral_code'.i18n; @@ -505,19 +472,29 @@ class _CheckoutState extends State } } - Future checkIfEmailExits() async { - try { - await sessionModel.checkEmailExists( - emailController.value.text, - ); - return false; - } catch (error, stackTrace) { - showError( - context, - error: error, - stackTrace: stackTrace, - ); - return false; + void resolveRoute() { + switch (widget.authFlow!) { + case AuthFlow.createAccount: + context.pushRoute(CreateAccountPassword( + email: widget.email.validateEmail, + code: widget.verificationPin!, + )); + break; + case AuthFlow.reset: + // TODO: Handle this case. + break; + case AuthFlow.signIn: + // TODO: Handle this case. + break; + + case AuthFlow.verifyEmail: + // TODO: Handle this case. + case AuthFlow.proCodeActivation: + // TODO: Handle this case. + case AuthFlow.changeEmail: + // TODO: Handle this case. + case AuthFlow.updateAccount: + // TODO: Handle this case. } } } diff --git a/lib/plans/plan_details.dart b/lib/plans/plan_details.dart index 47c78f793..d57c492df 100644 --- a/lib/plans/plan_details.dart +++ b/lib/plans/plan_details.dart @@ -132,8 +132,15 @@ class _PlanCardState extends State { resolveRouteIOS(); break; default: - // proceed to the default checkout page on Android and desktop - _checkOut(context); + if(Platform.isAndroid){ + _processCheckOut(context); + return; + } + if (widget.isPro) { + _processCheckOut(context); + } else { + signUpFlow(); + } break; } } @@ -150,7 +157,7 @@ class _PlanCardState extends State { return providers; } - Future _checkOut(BuildContext context) async { + Future _processCheckOut(BuildContext context) async { final isPlayVersion = sessionModel.isPlayVersion.value ?? false; final inRussia = sessionModel.country.value == 'RU'; // * Play version (Android only) @@ -182,17 +189,19 @@ class _PlanCardState extends State { } } + final email = sessionModel.userEmail.value; // * Proceed to our own Checkout await context.pushRoute( Checkout( plan: widget.plan, isPro: widget.isPro, + email: email, ), ); } void resolveRouteIOS() { - if (widget.isPro ) { + if (widget.isPro) { //user is signed in _proceedToCheckoutIOS(context); } else { diff --git a/lib/plans/plans.dart b/lib/plans/plans.dart index e38b6b172..76eb2b18d 100644 --- a/lib/plans/plans.dart +++ b/lib/plans/plans.dart @@ -62,23 +62,6 @@ class PlansPage extends StatelessWidget { ], ), ), - // * Step - Container( - color: white, - padding: const EdgeInsetsDirectional.only( - top: 16.0, - bottom: 16.0, - start: 32.0, - end: 32.0, - ), - child: Container( - margin: const EdgeInsetsDirectional.only(start: 4.0), - child: PlanStep( - stepNum: '1', - description: 'choose_plan'.i18n, - ), - ), - ), // * Card ...plans .toList() @@ -100,8 +83,6 @@ class PlansPage extends StatelessWidget { ], ), ), - // todo need to enable this for other platform soon - _buildFooter(context, proUser), ], ); @@ -114,10 +95,6 @@ class PlansPage extends StatelessWidget { ///If the user is already so not ask for email ///f the user is not pro, ask for email void _onPromoCodeTap(BuildContext context, bool proUser) { - if (!Platform.isIOS) { - context.pushRoute(ResellerCodeCheckoutLegacy(isPro: true)); - return; - } if (proUser) { context.pushRoute( ResellerCodeCheckout(isPro: true, email: sessionModel.userEmail.value!), diff --git a/lib/plans/utils.dart b/lib/plans/utils.dart index 7a8ac381f..3a1a40982 100644 --- a/lib/plans/utils.dart +++ b/lib/plans/utils.dart @@ -83,7 +83,7 @@ void showSuccessDialog( ); } -enum Providers { shepherd, stripe, btcpay, freekassa, fropay, paymentwall } +enum Providers { shepherd, stripe, btcpay, freekassa, fropay, paymentwall, test } extension ProviderExtension on String { Providers toPaymentEnum() { @@ -98,6 +98,8 @@ extension ProviderExtension on String { return Providers.paymentwall; case "shepherd": return Providers.shepherd; + case "test": + return Providers.test; default: return Providers.stripe; } diff --git a/pubspec.lock b/pubspec.lock index 6b3a61083..063b3e8ae 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -565,10 +565,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "2ca051989f69d1b2ca012b2cf3ccf78c70d40144f0861ff2c063493f7c8c3d45" + sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258" url: "https://pub.dev" source: hosted - version: "8.0.5" + version: "8.0.6" filesize: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 4c51fd1b1..24de8aa5a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,7 +58,7 @@ dependencies: audioplayers: ^6.0.0 video_player: ^2.7.0 video_thumbnail: ^0.5.3 - file_picker: ^8.0.5 + file_picker: ^8.0.6 filesize: ^2.0.1 flutter_image_compress: ^2.3.0