diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000..83994b6a0 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,27 @@ +# template for auto-generated release notes +# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes + +changelog: + # exclude: + # labels: + # - ignore-for-release + # authors: + # - octocat + categories: + - title: ✨ Major Features + labels: + - epic + - title: 🪲 Bug Fixes + labels: + - bug + - cs + - title: 📱 Team Apps + labels: + - team_apps + - title: 📶 Team SDK + labels: + - team_sdk + - title: 🛠️ Team Infra + labels: + - team_infra + - telemetry diff --git a/.github/workflows/release-changelog.yml b/.github/workflows/release-changelog.yml new file mode 100644 index 000000000..6a8e893db --- /dev/null +++ b/.github/workflows/release-changelog.yml @@ -0,0 +1,42 @@ +name: Release and Changelog + +on: + workflow_dispatch: + push: + tags: + # a prerelease noted by a hyphen will not trigger + - 'lantern-[0-9]+.[0-9]+.[0-9]+' + +jobs: + create-release: + runs-on: ubuntu-latest + steps: + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + # https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release + run: | + gh api -i \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository }}/releases \ + -f tag_name='${{ github.ref_name }}' \ + -f name='Release ${{ github.ref_name }}' \ + -f body='🤖 Autogenerated changelog for ${{ github.repository }} ${{ github.ref_name }}' \ + -F draft=false \ + -F prerelease=false \ + -F generate_release_notes=true + - name: Strip repository name of owner + id: repo + run: | + echo "REPO_NAME=$(echo ${{ github.repository }} | sed s/'${{ github.repository_owner }}\/'//g)" >> "$GITHUB_OUTPUT" + - name: Trigger QC Checklist in customer-service repo + env: + GH_GRANTS_TOKEN: ${{ secrets.GH_GRANTS_TOKEN }} + run: | + echo "Sending QC Checklist trigger for ${{ steps.repo.outputs.REPO_NAME }} ${{ github.ref_name }}" + curl -i -H "Accept: application/vnd.github.everest-preview+json" \ + -H "Authorization: token $GH_GRANTS_TOKEN" \ + --request POST --data '{"event_type": "TRIGGER_QC_CHECKLIST", "client_payload": { "platform": "${{ steps.repo.outputs.REPO_NAME }}", "version": "${{ github.ref_name }}"}}' \ + https://api.github.com/repos/getlantern/customer-support/dispatches diff --git a/android/app/src/main/java/org/getlantern/lantern/model/ProPlan.java b/android/app/src/main/java/org/getlantern/lantern/model/ProPlan.java index 62afaf7fe..ba1cd6977 100644 --- a/android/app/src/main/java/org/getlantern/lantern/model/ProPlan.java +++ b/android/app/src/main/java/org/getlantern/lantern/model/ProPlan.java @@ -288,7 +288,7 @@ public String getFormattedPrice(Map price) { return getFormattedPrice(price, false); } - private String getFormattedPrice(Map price, boolean formatFloat) { + private String getFormattedPrice(Map price, boolean formatFloat) { final String formattedPrice; Long currencyPrice = price.get(currencyCode); if (currencyPrice == null) { diff --git a/go.mod b/go.mod index 5dcd65ba9..05006ab76 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ replace github.com/google/netstack => github.com/getlantern/netstack v0.0.0-2022 replace github.com/eycorsican/go-tun2socks => github.com/getlantern/go-tun2socks v1.16.12-0.20201218023150-b68f09e5ae93 require ( + github.com/bojanz/currency v1.1.3 github.com/getlantern/appdir v0.0.0-20200615192800-a0ef1968f4da github.com/getlantern/autoupdate v0.0.0-20231030193554-30131726a6d9 github.com/getlantern/dnsgrab v0.0.0-20230822102054-7ff232ec3148 @@ -87,6 +88,7 @@ require ( github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/siphash v1.2.3 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect diff --git a/go.sum b/go.sum index 776141241..963764a3e 100644 --- a/go.sum +++ b/go.sum @@ -186,6 +186,8 @@ github.com/bits-and-blooms/bitset v1.3.0 h1:h7mv5q31cthBTd7V4kLAZaIThj1e8vPGcSqp github.com/bits-and-blooms/bitset v1.3.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bojanz/currency v1.1.3 h1:Ie8LYoMegQQlXH5Lq/gMRaWAD8J2/DYPewv+JV6UTPs= +github.com/bojanz/currency v1.1.3/go.mod h1:jNoZiJyRTqoU5DFoa+n+9lputxPUDa8Fz8BdDrW06Go= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= @@ -205,6 +207,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudfoundry/gosigar v1.1.0/go.mod h1:3qLfc2GlfmwOx2+ZDaRGH3Y9fwQ0sQeaAleo2GV5pH0= github.com/cloudfoundry/gosigar v1.2.0/go.mod h1:3qLfc2GlfmwOx2+ZDaRGH3Y9fwQ0sQeaAleo2GV5pH0= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/colinmarc/hdfs/v2 v2.1.1/go.mod h1:M3x+k8UKKmxtFu++uAZ0OtDU8jR3jnaZIAc6yK4Ue0c= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -728,6 +732,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= diff --git a/internalsdk/apimodels/api.go b/internalsdk/apimodels/api.go index 9c5803c13..383da6645 100644 --- a/internalsdk/apimodels/api.go +++ b/internalsdk/apimodels/api.go @@ -12,6 +12,15 @@ const ( baseUrl = "https://api.getiantem.org" userDetailUrl = baseUrl + "/user-data" userCreateUrl = baseUrl + "/user-create" + plansV3Url = baseUrl + "/plans-v3" + purchaseUrl = baseUrl + "/purchase" +) + +const ( + headerDeviceId = "X-Lantern-Device-Id" + headerUserId = "X-Lantern-User-Id" + headerProToken = "X-Lantern-Pro-Token" + headerContentType = "Content-Type" ) var ( @@ -28,9 +37,9 @@ func FechUserDetail(deviceId string, userId string, token string) (*UserDetailRe } // Add headers - req.Header.Set("X-Lantern-Device-Id", deviceId) - req.Header.Set("X-Lantern-User-Id", userId) - req.Header.Set("X-Lantern-Pro-Token", token) + req.Header.Set(headerDeviceId, deviceId) + req.Header.Set(headerUserId, userId) + req.Header.Set(headerProToken, token) log.Debugf("Headers set") // Initialize a new http client @@ -75,7 +84,7 @@ func UserCreate(deviceId string, local string) (*UserResponse, error) { } // Add headers - req.Header.Set("X-Lantern-Device-Id", deviceId) + req.Header.Set(headerDeviceId, deviceId) log.Debugf("Headers set") // Initialize a new http client client := &http.Client{} @@ -94,3 +103,90 @@ func UserCreate(deviceId string, local string) (*UserResponse, error) { } return &userResponse, nil } + +func PlansV3(deviceId string, userId string, local string, token string, countryCode string) (*PlansResponse, error) { + req, err := http.NewRequest("GET", plansV3Url, nil) + if err != nil { + log.Errorf("Error creating plans request: %v", err) + return nil, err + } + //Add query params + q := req.URL.Query() + q.Add("locale", local) + q.Add("countrycode", countryCode) + req.URL.RawQuery = q.Encode() + + // Add headers + req.Header.Set(headerDeviceId, deviceId) + req.Header.Set(headerUserId, userId) + req.Header.Set(headerProToken, token) + log.Debugf("Plans Headers set") + // Initialize a new http client + client := &http.Client{} + // Send the request + resp, err := client.Do(req) + if err != nil { + log.Errorf("Error sending plans request: %v", err) + return nil, err + } + + defer resp.Body.Close() + + var plans PlansResponse + // Read and decode the response body + if err := json.NewDecoder(resp.Body).Decode(&plans); err != nil { + log.Errorf("Error decoding response body: %v", err) + return nil, err + } + return &plans, nil +} + +func PurchaseRequest(data map[string]string, deviceId string, userId string, token string) (*PurchaseResponse, error) { + log.Debugf("purchase request body %v", data) + body, err := createJsonBody(data) + if err != nil { + log.Errorf("Error while creating json body") + return nil, err + } + + log.Debugf("Encoded body %v", body) + // Create a new request + req, err := http.NewRequest("POST", purchaseUrl, body) + if err != nil { + log.Errorf("Error creating new request: %v", err) + return nil, err + } + + // Add headers + req.Header.Set(headerDeviceId, deviceId) + req.Header.Set(headerUserId, userId) + req.Header.Set(headerProToken, token) + req.Header.Set(headerContentType, "application/json") + + client := &http.Client{} + // Send the request + resp, err := client.Do(req) + if err != nil { + log.Errorf("Error sending puchase request: %v", err) + return nil, err + } + + defer resp.Body.Close() + + var purchase PurchaseResponse + // Read and decode the response body + if err := json.NewDecoder(resp.Body).Decode(&purchase); err != nil { + log.Errorf("Error decoding response body: %v", err) + return nil, err + } + return &purchase, nil +} + +// Utils methods convert json body +func createJsonBody(data map[string]string) (*bytes.Buffer, error) { + jsonData, err := json.Marshal(data) + if err != nil { + return nil, err + } + return bytes.NewBuffer(jsonData), nil +} diff --git a/internalsdk/apimodels/json_models.go b/internalsdk/apimodels/json_models.go index 659764345..0d6bbe360 100644 --- a/internalsdk/apimodels/json_models.go +++ b/internalsdk/apimodels/json_models.go @@ -42,3 +42,75 @@ type UserDevice struct { Name string `json:"name"` Created int64 `json:"created"` } + +// Plans Json struct + +type PlansResponse struct { + Plans []Plan `json:"plans"` + Providers Providers `json:"providers"` +} + +type Plan struct { + ID string `json:"id"` + Description string `json:"description"` + Duration Duration `json:"duration"` + Price map[string]int64 `json:"price"` + ExpectedMonthlyPrice map[string]int64 `json:"expectedMonthlyPrice"` + UsdPrice int64 `json:"usdPrice"` + UsdPrice1Y int64 `json:"usdPrice1Y"` + UsdPrice2Y int64 `json:"usdPrice2Y"` + RedeemFor RedeemFor `json:"redeemFor"` + RenewalBonus RedeemFor `json:"renewalBonus"` + RenewalBonusExpired RedeemFor `json:"renewalBonusExpired"` + RenewalBonusExpected RedeemFor `json:"renewalBonusExpected"` + Discount float64 `json:"discount"` + BestValue bool `json:"bestValue"` + Level string `json:"level"` + TotalCostBilledOneTime string + FormattedDiscount string + FormattedBonus string + OneMonthCost string + TotalCost string +} + +type Price struct { + Usd int64 `json:"usd"` +} + +type Duration struct { + Days int64 `json:"days"` + Months int64 `json:"months"` + Years int64 `json:"years"` +} + +type RedeemFor struct { + Days int64 `json:"days"` + Months int64 `json:"months"` +} + +type Providers struct { + Android []Android `json:"android"` + Desktop []Android `json:"desktop"` +} + +type Android struct { + Method string `json:"method"` + Providers []Provider `json:"providers"` +} + +type Provider struct { + Name string `json:"name"` + Data *Data `json:"data,omitempty"` +} + +type Data struct { + PubKey string `json:"pubKey"` +} + +//Purchase Request + +type PurchaseResponse struct { + PaymentStatus string `json:"paymentStatus"` + Plan Plan `json:"plan"` + Status string `json:"status"` +} diff --git a/internalsdk/protos/vpn.pb.go b/internalsdk/protos/vpn.pb.go index f4bd18172..0046bf2b4 100644 --- a/internalsdk/protos/vpn.pb.go +++ b/internalsdk/protos/vpn.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 -// protoc v4.23.3 +// protoc-gen-go v1.26.0 +// protoc v4.23.4 // source: protos_shared/vpn.proto package protos @@ -335,6 +335,53 @@ func (x *Devices) GetDevices() []*Device { return nil } +type Plans struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Plan []*Plan `protobuf:"bytes,1,rep,name=plan,proto3" json:"plan,omitempty"` +} + +func (x *Plans) Reset() { + *x = Plans{} + if protoimpl.UnsafeEnabled { + mi := &file_protos_shared_vpn_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Plans) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Plans) ProtoMessage() {} + +func (x *Plans) ProtoReflect() protoreflect.Message { + mi := &file_protos_shared_vpn_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Plans.ProtoReflect.Descriptor instead. +func (*Plans) Descriptor() ([]byte, []int) { + return file_protos_shared_vpn_proto_rawDescGZIP(), []int{5} +} + +func (x *Plans) GetPlan() []*Plan { + if x != nil { + return x.Plan + } + return nil +} + type Plan struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -355,7 +402,7 @@ type Plan struct { func (x *Plan) Reset() { *x = Plan{} if protoimpl.UnsafeEnabled { - mi := &file_protos_shared_vpn_proto_msgTypes[5] + mi := &file_protos_shared_vpn_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -368,7 +415,7 @@ func (x *Plan) String() string { func (*Plan) ProtoMessage() {} func (x *Plan) ProtoReflect() protoreflect.Message { - mi := &file_protos_shared_vpn_proto_msgTypes[5] + mi := &file_protos_shared_vpn_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -381,7 +428,7 @@ func (x *Plan) ProtoReflect() protoreflect.Message { // Deprecated: Use Plan.ProtoReflect.Descriptor instead. func (*Plan) Descriptor() ([]byte, []int) { - return file_protos_shared_vpn_proto_rawDescGZIP(), []int{5} + return file_protos_shared_vpn_proto_rawDescGZIP(), []int{6} } func (x *Plan) GetId() string { @@ -465,7 +512,7 @@ type PaymentProviders struct { func (x *PaymentProviders) Reset() { *x = PaymentProviders{} if protoimpl.UnsafeEnabled { - mi := &file_protos_shared_vpn_proto_msgTypes[6] + mi := &file_protos_shared_vpn_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -478,7 +525,7 @@ func (x *PaymentProviders) String() string { func (*PaymentProviders) ProtoMessage() {} func (x *PaymentProviders) ProtoReflect() protoreflect.Message { - mi := &file_protos_shared_vpn_proto_msgTypes[6] + mi := &file_protos_shared_vpn_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -491,7 +538,7 @@ func (x *PaymentProviders) ProtoReflect() protoreflect.Message { // Deprecated: Use PaymentProviders.ProtoReflect.Descriptor instead. func (*PaymentProviders) Descriptor() ([]byte, []int) { - return file_protos_shared_vpn_proto_rawDescGZIP(), []int{6} + return file_protos_shared_vpn_proto_rawDescGZIP(), []int{7} } func (x *PaymentProviders) GetName() string { @@ -513,7 +560,7 @@ type PaymentMethod struct { func (x *PaymentMethod) Reset() { *x = PaymentMethod{} if protoimpl.UnsafeEnabled { - mi := &file_protos_shared_vpn_proto_msgTypes[7] + mi := &file_protos_shared_vpn_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -526,7 +573,7 @@ func (x *PaymentMethod) String() string { func (*PaymentMethod) ProtoMessage() {} func (x *PaymentMethod) ProtoReflect() protoreflect.Message { - mi := &file_protos_shared_vpn_proto_msgTypes[7] + mi := &file_protos_shared_vpn_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -539,7 +586,7 @@ func (x *PaymentMethod) ProtoReflect() protoreflect.Message { // Deprecated: Use PaymentMethod.ProtoReflect.Descriptor instead. func (*PaymentMethod) Descriptor() ([]byte, []int) { - return file_protos_shared_vpn_proto_rawDescGZIP(), []int{7} + return file_protos_shared_vpn_proto_rawDescGZIP(), []int{8} } func (x *PaymentMethod) GetMethod() string { @@ -589,43 +636,45 @@ var file_protos_shared_vpn_proto_rawDesc = []byte{ 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x22, 0x2c, 0x0a, 0x07, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x07, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x07, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22, 0x98, 0x03, 0x0a, 0x04, 0x50, 0x6c, 0x61, 0x6e, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x65, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x62, 0x65, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x64, 0x50, 0x72, 0x69, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x08, 0x75, 0x73, 0x64, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x26, 0x0a, 0x05, - 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x50, 0x6c, - 0x61, 0x6e, 0x2e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x70, - 0x72, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x73, - 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x4f, 0x6e, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x73, 0x74, 0x42, - 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x4f, 0x6e, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, - 0x6f, 0x6e, 0x65, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x43, 0x6f, 0x73, 0x74, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x6e, 0x65, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x43, 0x6f, 0x73, 0x74, - 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x73, 0x74, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x26, - 0x0a, 0x0e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x74, 0x65, 0x64, 0x42, 0x6f, 0x6e, 0x75, 0x73, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x74, 0x65, - 0x64, 0x42, 0x6f, 0x6e, 0x75, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, - 0x6c, 0x54, 0x65, 0x78, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x72, 0x65, 0x6e, - 0x65, 0x77, 0x61, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x1a, 0x38, 0x0a, 0x0a, 0x50, 0x72, 0x69, 0x63, - 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x22, 0x26, 0x0a, 0x10, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x58, 0x0a, 0x0d, 0x50, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x6d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x12, 0x2f, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x73, 0x42, 0x1b, 0x0a, 0x10, 0x69, 0x6f, 0x2e, 0x6c, 0x61, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5a, 0x07, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22, 0x22, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x6e, 0x73, + 0x12, 0x19, 0x0a, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x05, + 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x04, 0x70, 0x6c, 0x61, 0x6e, 0x22, 0x98, 0x03, 0x0a, 0x04, + 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x62, 0x65, 0x73, 0x74, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x62, 0x65, 0x73, 0x74, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x64, 0x50, 0x72, 0x69, 0x63, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x75, 0x73, 0x64, 0x50, 0x72, 0x69, 0x63, 0x65, + 0x12, 0x26, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x10, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x16, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x43, 0x6f, 0x73, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x4f, 0x6e, 0x65, 0x54, 0x69, + 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, + 0x6f, 0x73, 0x74, 0x42, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x4f, 0x6e, 0x65, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x22, 0x0a, 0x0c, 0x6f, 0x6e, 0x65, 0x4d, 0x6f, 0x6e, 0x74, 0x68, 0x43, 0x6f, 0x73, 0x74, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x6e, 0x65, 0x4d, 0x6f, 0x6e, 0x74, 0x68, + 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, 0x73, + 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x43, 0x6f, + 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x74, 0x65, 0x64, 0x42, + 0x6f, 0x6e, 0x75, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x74, 0x65, 0x64, 0x42, 0x6f, 0x6e, 0x75, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x72, 0x65, + 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x61, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x1a, 0x38, 0x0a, 0x0a, + 0x50, 0x72, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x26, 0x0a, 0x10, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x58, + 0x0a, 0x0d, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, + 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x2f, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x52, 0x09, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x42, 0x1b, 0x0a, 0x10, 0x69, 0x6f, 0x2e, 0x6c, + 0x61, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x5a, 0x07, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -640,27 +689,29 @@ func file_protos_shared_vpn_proto_rawDescGZIP() []byte { return file_protos_shared_vpn_proto_rawDescData } -var file_protos_shared_vpn_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_protos_shared_vpn_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_protos_shared_vpn_proto_goTypes = []interface{}{ (*ServerInfo)(nil), // 0: ServerInfo (*Bandwidth)(nil), // 1: Bandwidth (*AppData)(nil), // 2: AppData (*Device)(nil), // 3: Device (*Devices)(nil), // 4: Devices - (*Plan)(nil), // 5: Plan - (*PaymentProviders)(nil), // 6: PaymentProviders - (*PaymentMethod)(nil), // 7: PaymentMethod - nil, // 8: Plan.PriceEntry + (*Plans)(nil), // 5: Plans + (*Plan)(nil), // 6: Plan + (*PaymentProviders)(nil), // 7: PaymentProviders + (*PaymentMethod)(nil), // 8: PaymentMethod + nil, // 9: Plan.PriceEntry } var file_protos_shared_vpn_proto_depIdxs = []int32{ 3, // 0: Devices.devices:type_name -> Device - 8, // 1: Plan.price:type_name -> Plan.PriceEntry - 6, // 2: PaymentMethod.providers:type_name -> PaymentProviders - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 6, // 1: Plans.plan:type_name -> Plan + 9, // 2: Plan.price:type_name -> Plan.PriceEntry + 7, // 3: PaymentMethod.providers:type_name -> PaymentProviders + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_protos_shared_vpn_proto_init() } @@ -730,7 +781,7 @@ func file_protos_shared_vpn_proto_init() { } } file_protos_shared_vpn_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Plan); i { + switch v := v.(*Plans); i { case 0: return &v.state case 1: @@ -742,7 +793,7 @@ func file_protos_shared_vpn_proto_init() { } } file_protos_shared_vpn_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PaymentProviders); i { + switch v := v.(*Plan); i { case 0: return &v.state case 1: @@ -754,6 +805,18 @@ func file_protos_shared_vpn_proto_init() { } } file_protos_shared_vpn_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PaymentProviders); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_protos_shared_vpn_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*PaymentMethod); i { case 0: return &v.state @@ -772,7 +835,7 @@ func file_protos_shared_vpn_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_protos_shared_vpn_proto_rawDesc, NumEnums: 0, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/internalsdk/session_model.go b/internalsdk/session_model.go index 724bf9d98..07e05acd5 100644 --- a/internalsdk/session_model.go +++ b/internalsdk/session_model.go @@ -4,6 +4,8 @@ import ( "fmt" "path/filepath" "strconv" + "strings" + "time" "github.com/getlantern/android-lantern/internalsdk/apimodels" "github.com/getlantern/android-lantern/internalsdk/protos" @@ -20,6 +22,16 @@ type SessionModel struct { *baseModel } +// Expose payment providers +const ( + paymentProviderStripe = "stripe" + paymentProviderFreekassa = "freekassa" + paymentProviderGooglePlay = "googleplay" + paymentProviderApplePay = "applepay" + paymentProviderBTCPay = "btcpay" + paymentProviderResellerCode = "reseller-code" +) + // List of we are using for Session Model const ( pathDeviceID = "deviceid" @@ -57,6 +69,10 @@ const ( pathStoreVersion = "storeVersion" pathSelectedTab = "/selectedTab" pathServerInfo = "/server_info" + pathPlans = "/plans/" + pathResellerCode = "resellercode" + pathExpirydate = "expirydate" + pathExpirystr = "expirydatestr" currentTermsVersion = 1 ) @@ -82,6 +98,9 @@ func NewSessionModel(mdb minisql.DB, opts *SessionModelOpts) (*SessionModel, err } base.db.RegisterType(1000, &protos.ServerInfo{}) base.db.RegisterType(2000, &protos.Devices{}) + base.db.RegisterType(5000, &protos.Device{}) + base.db.RegisterType(3000, &protos.Plan{}) + base.db.RegisterType(4000, &protos.Plans{}) m := &SessionModel{baseModel: base} m.baseModel.doInvokeMethod = m.doInvokeMethod return m, m.initSessionModel(opts) @@ -129,6 +148,8 @@ func (m *SessionModel) doInvokeMethod(method string, arguments Arguments) (inter if err != nil { return nil, err } + //Todo find way to call PLans api everytime user chnage lang + //So plans will apper in there local lang return true, nil case "acceptTerms": err := acceptTerms(m.baseModel) @@ -157,12 +178,24 @@ func (m *SessionModel) doInvokeMethod(method string, arguments Arguments) (inter return nil, err } return true, nil - case "createUser": - err := userCreate(m.baseModel, arguments.Scalar().String()) + case "redeemResellerCode": + email := arguments.Get("email").String() + resellerCode := arguments.Get("resellerCode").String() + err := redeemResellerCode(m, email, resellerCode) + if err != nil { + return nil, err + } + return true, nil + + case "submitApplePayPayment": + plandId := arguments.Get("planID").String() + purchaseId := arguments.Get("purchaseId").String() + err := submitApplePayPayment(m, plandId, purchaseId) if err != nil { return nil, err } return true, nil + default: return m.methodNotImplemented(method) } @@ -256,6 +289,48 @@ func (m *SessionModel) initSessionModel(opts *SessionModelOpts) error { if err != nil { return err } + + token, err := m.GetToken() + if err != nil { + return err + } + countryCode, err := m.GetCountryCode() + if err != nil { + return err + } + //Get all the Plans + userIdStr := fmt.Sprintf("%d", userId) + if userId == 0 { + tempUserId, err := m.GetUserID() + if err != nil { + return err + } + userIdStr = fmt.Sprintf("%d", tempUserId) + } + + err = getPlansV3(m.baseModel, opts.DeviceID, userIdStr, lang, token, countryCode) + if err != nil { + log.Debugf("Plans V3 error: %v", err) + return err + } + return nil +} + +func getPlansV3(m *baseModel, deviceId string, userId string, lang string, token string, countyCode string) error { + // + log.Debugf("Request data deviceID %v userId %v lang %v token %v countyCode %v", deviceId, userId, lang, token, countyCode) + plans, err := apimodels.PlansV3(deviceId, userId, lang, token, countyCode) + if err != nil { + log.Debugf("Plans V3 error: %v", err) + return err + } + log.Debugf("Plans V3 response: %+v", plans) + + /// Process Plans and providers + err = storePlanDetail(m, *plans) + if err != nil { + return err + } return nil } @@ -361,6 +436,23 @@ func setUserLevel(m *baseModel, userLevel string) error { return pathdb.Put(tx, pathUserLevel, userLevel, "") }) } +func setExpiration(m *baseModel, expiration int64) error { + if expiration == 0 { + return nil + } + + expiry := time.Unix(0, expiration*int64(time.Second)) + dateFormat := "01/02/2006" + dateStr := expiry.Format(dateFormat) + + return pathdb.Mutate(m.db, func(tx pathdb.TX) error { + err := pathdb.Put[string](tx, pathExpirystr, dateStr, "") + if err != nil { + return err + } + return pathdb.Put[int64](tx, pathExpirydate, expiration, "") + }) +} func getUserLevel(m *baseModel) (string, error) { return pathdb.Get[string](m.db, pathUserLevel) @@ -381,6 +473,7 @@ func setLanguage(m *baseModel, lang string) error { } func setDevices(m *baseModel, devices []apimodels.UserDevice) error { + log.Debugf("Device list %v", devices) var protoDevices []*protos.Device for _, device := range devices { protoDevice := &protos.Device{ @@ -391,13 +484,65 @@ func setDevices(m *baseModel, devices []apimodels.UserDevice) error { protoDevices = append(protoDevices, protoDevice) } + userDevice := &protos.Devices{Devices: protoDevices} pathdb.Mutate(m.db, func(tx pathdb.TX) error { - pathdb.Put(tx, pathDevices, protoDevices, "") + pathdb.Put(tx, pathDevices, userDevice, "") return nil }) + log.Debugf("Device stored successfully") + return nil +} + +func storePlanDetail(m *baseModel, plan apimodels.PlansResponse) error { + log.Debugf("Storing Plan details ") + err := setPlans(m, plan.Plans) + if err != nil { + return err + } + log.Debugf("Plan details stored successful") return nil } +func setPlans(m *baseModel, plans []apimodels.Plan) error { + + return pathdb.Mutate(m.db, func(tx pathdb.TX) error { + //Get local from user + lang, err := pathdb.Get[string](tx, pathLang) + if err != nil { + return err + } + + for _, plans := range plans { + // Update priceing for each plan + err := updatePrice(&plans, lang) + if err != nil { + log.Debugf("Error while updateing price") + return err + } + log.Debugf("Plans Values %+v", plans) + pathPlanId := pathPlans + strings.Split(plans.ID, "-")[0] + protoPlan := &protos.Plan{ + Id: plans.ID, + Description: plans.Description, + BestValue: plans.BestValue, + UsdPrice: plans.UsdPrice, + TotalCostBilledOneTime: plans.TotalCostBilledOneTime, + Price: plans.Price, + OneMonthCost: plans.OneMonthCost, + TotalCost: plans.TotalCost, + FormattedBonus: plans.FormattedBonus, + RenewalText: "", + } + err = pathdb.Put(tx, pathPlanId, protoPlan, "") + if err != nil { + log.Debugf("Error while addding price") + return err + } + } + return nil + }) +} + func (m *SessionModel) GetTimeZone() (string, error) { return pathdb.Get[string](m.baseModel.db, pathTimezoneID) } @@ -561,6 +706,11 @@ func setUserIdAndToken(m *baseModel, userId int, token string) error { return pathdb.Put(tx, pathToken, token, "") }) } +func setResellerCode(m *baseModel, resellerCode string) error { + return pathdb.Mutate(m.db, func(tx pathdb.TX) error { + return pathdb.Put(tx, pathResellerCode, resellerCode, "") + }) +} // Create user // Todo-: Create Sprate http client to manag and reuse client @@ -619,6 +769,7 @@ func cacheUserDetail(m *baseModel, userDetail *apimodels.UserDetailResponse) err return err } } + if userDetail.UserStatus != "" && userDetail.UserStatus == "active" && userDetail.UserLevel == "pro" { setProUser(m, true) } else { @@ -629,6 +780,11 @@ func cacheUserDetail(m *baseModel, userDetail *apimodels.UserDetailResponse) err return err } + err = setExpiration(m, userDetail.Expiration) + if err != nil { + return err + } + //Store all device err = setDevices(m, userDetail.Devices) if err != nil { @@ -674,3 +830,79 @@ func reportIssue(session *SessionModel, email string, issue string, description log.Debugf("Report an issue index %v desc %v level %v email %v, device %v model %v version %v ", issueKey, description, level, email, device, model, osVersion) return SendIssueReport(session, issueKey, description, level, email, device, model, osVersion) } + +func redeemResellerCode(m *SessionModel, email string, resellerCode string) error { + err := setEmail(m.baseModel, email) + if err != nil { + log.Errorf("Error while setting email %v", err) + return err + } + setResellerCode(m.baseModel, resellerCode) + if err != nil { + log.Errorf("Error while setting resellerCode %v", err) + return err + } + + err, purchaseData := createPurchaseData(m, paymentProviderResellerCode, resellerCode, "", "") + if err != nil { + log.Errorf("Error while creating purchase data %v", err) + return err + } + + deviecId, err := m.GetDeviceID() + if err != nil { + return err + } + userId, err := m.GetUserID() + if err != nil { + return err + } + userIdStr := fmt.Sprintf("%d", userId) + + token, err := m.GetToken() + if err != nil { + return err + } + purchase, err := apimodels.PurchaseRequest(purchaseData, deviecId, userIdStr, token) + if err != nil { + return err + } + log.Debugf("Purchase Request response %v", purchase) + + // Set user to pro + return setProUser(m.baseModel, true) +} + +func submitApplePayPayment(m *SessionModel, planId string, purchaseToken string) error { + log.Debugf("Submit Apple Pay Payment planId %v purchaseToken %v", planId, purchaseToken) + err, purchaseData := createPurchaseData(m, paymentProviderApplePay, "", purchaseToken, planId) + if err != nil { + log.Errorf("Error while creating purchase data %v", err) + return err + } + deviecId, err := m.GetDeviceID() + if err != nil { + return err + } + userId, err := m.GetUserID() + if err != nil { + return err + } + userIdStr := fmt.Sprintf("%d", userId) + + token, err := m.GetToken() + if err != nil { + return err + } + purchase, err := apimodels.PurchaseRequest(purchaseData, deviecId, userIdStr, token) + if err != nil { + return err + } + log.Debugf("Purchase Request response %+v", purchase) + + if purchase.Status != "ok" { + return errors.New("Purchase Request failed") + } + // Set user to pro + return setProUser(m.baseModel, true) +} diff --git a/internalsdk/utils.go b/internalsdk/utils.go index 152e0af02..e84db8244 100644 --- a/internalsdk/utils.go +++ b/internalsdk/utils.go @@ -2,9 +2,16 @@ package internalsdk import ( "encoding/binary" + "fmt" "math" + "strconv" + "strings" + "time" + "github.com/bojanz/currency" + "github.com/getlantern/android-lantern/internalsdk/apimodels" "github.com/getlantern/errors" + "github.com/getlantern/pathdb" ) func BytesToFloat64LittleEndian(b []byte) (float64, error) { @@ -14,3 +21,119 @@ func BytesToFloat64LittleEndian(b []byte) (float64, error) { bits := binary.LittleEndian.Uint64(b) return math.Float64frombits(bits), nil } + +func updatePrice(plan *apimodels.Plan, local string) error { + bonous := plan.RenewalBonusExpected + formattedBouns := formatRenewalBonusExpected(bonous.Months, bonous.Days, false) + log.Debugf("updateprice formattedBouns %v", formattedBouns) + totalCost, err := formatPrice(plan.Price, local) + log.Debugf("updateprice Total cost %v", totalCost) + if err != nil { + return err + } + //One Month Price + oneMonthCost, err := formatPrice(plan.ExpectedMonthlyPrice, local) + if err != nil { + return err + } + log.Debugf("updateprice oneMonthCost %v", oneMonthCost) + var formattedDiscount string + if plan.Discount > 0 { + discountPercentage := math.Round(plan.Discount * 100) + formattedDiscount = fmt.Sprintf("Save: %v%%", discountPercentage) + } + + plan.TotalCostBilledOneTime = fmt.Sprintf("%v billed one time", totalCost) + plan.OneMonthCost = oneMonthCost + plan.FormattedBonus = formattedBouns + plan.FormattedDiscount = formattedDiscount + plan.TotalCost = totalCost + return nil +} + +func formatPrice(price map[string]int64, local string) (string, error) { + locale := currency.NewLocale(local) + formatter := currency.NewFormatter(locale) + formatter.MaxDigits = 2 + + for currencyCode, amount := range price { + amountStr := fmt.Sprintf("%.2f", float64(amount)/100.00) + log.Debugf("Amount is %v", amountStr) + amount, err := currency.NewAmount(amountStr, strings.ToUpper(currencyCode)) + if err != nil { + return "", err + } + log.Debugf("Formated price is %v", formatter.Format(amount)) + return strings.ToUpper(currencyCode) + formatter.Format(amount), nil + } + return "", nil +} + +func formatRenewalBonusExpected(months int64, days int64, longForm bool) string { + var bonusParts []string + if months == 0 && days == 0 { + return "" + } + if longForm { + // "1 month and 15 days" + if months > 0 { + monthStr := "month" + if months > 1 { + monthStr = "months" + } + bonusParts = append(bonusParts, fmt.Sprintf("%d %s", months, monthStr)) + } + if days > 0 { + dayStr := "day" + if days > 1 { + dayStr = "days" + } + bonusParts = append(bonusParts, fmt.Sprintf("%d %s", days, dayStr)) + } + return strings.Join(bonusParts, " and ") + } else { + totalDays := months*30 + days + dayStr := "day" + if totalDays > 1 { + dayStr = "days" + } + return fmt.Sprintf("%d %s", totalDays, dayStr) + } +} + +//Create Purchase Request + +func createPurchaseData(session *SessionModel, paymentProvider string, resellerCode string, purchaseToken string, planId string) (error, map[string]string) { + email, err := session.Email() + if err != nil { + return err, nil + } + + device, err := pathdb.Get[string](session.db, pathModel) + if err != nil { + return err, nil + } + + data := map[string]string{ + "idempotencyKey": strconv.FormatInt(time.Now().UnixNano(), 10), + "provider": paymentProvider, + "email": email, + "deviceName": device, + } + + switch paymentProvider { + case paymentProviderResellerCode: + data["provider"] = paymentProviderResellerCode + data["resellerCode"] = resellerCode + data["currency"] = "usd" + data["plan"] = planId + case paymentProviderApplePay: + data["token"] = purchaseToken + data["currency"] = "usd" + data["plan"] = planId + + } + + return nil, data + +} diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 80b7c6a43..1277dd5a5 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -110,6 +110,9 @@ PODS: - "GoogleUtilities/NSData+zlib (7.11.5)" - GoogleUtilities/Reachability (7.11.5): - GoogleUtilities/Logger + - in_app_purchase_storekit (0.0.1): + - Flutter + - FlutterMacOS - integration_test (0.0.1): - Flutter - libwebp (1.3.2): @@ -192,6 +195,7 @@ DEPENDENCIES: - flutter_uploader (from `.symlinks/plugins/flutter_uploader/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - google_mobile_ads (from `.symlinks/plugins/google_mobile_ads/ios`) + - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -259,6 +263,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluttertoast/ios" google_mobile_ads: :path: ".symlinks/plugins/google_mobile_ads/ios" + in_app_purchase_storekit: + :path: ".symlinks/plugins/in_app_purchase_storekit/darwin" integration_test: :path: ".symlinks/plugins/integration_test/ios" package_info_plus: @@ -310,6 +316,7 @@ SPEC CHECKSUMS: GoogleAppMeasurement: 722db6550d1e6d552b08398b69a975ac61039338 GoogleUserMessagingPlatform: dce302b8f1b84d6e945812ee7a15c3f65a102cbf GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 + in_app_purchase_storekit: 4fb7ee9e824b1f09107fbfbbce8c4b276366dc43 integration_test: 13825b8a9334a850581300559b8839134b124670 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb diff --git a/ios/Runner/Lantern/Models/SessionModel.swift b/ios/Runner/Lantern/Models/SessionModel.swift index a068ddffe..252bf2594 100644 --- a/ios/Runner/Lantern/Models/SessionModel.swift +++ b/ios/Runner/Lantern/Models/SessionModel.swift @@ -30,7 +30,7 @@ class SessionModel: BaseModel { opts.playVersion = (isRunningFromAppStore() || isRunningInTestFlightEnvironment()) opts.timeZone = TimeZone.current.identifier opts.device = systemName // IOS does not provide Device name directly - opts.model = systemName + opts.model = model opts.osVersion = systemVersion opts.paymentTestMode = AppEnvironment.current == AppEnvironment.appiumTest diff --git a/lib/account/account.dart b/lib/account/account.dart index aad9669ab..8ced5fbf9 100644 --- a/lib/account/account.dart +++ b/lib/account/account.dart @@ -3,7 +3,7 @@ import 'package:lantern/messaging/messaging_model.dart'; @RoutePage(name: 'Account') class AccountMenu extends StatelessWidget { - const AccountMenu({Key? key}) : super(key: key); + const AccountMenu({Key? key}) : super(key: key); Future authorizeDeviceForPro(BuildContext context) async => await context.pushRoute(AuthorizePro()); @@ -48,15 +48,14 @@ class AccountMenu extends StatelessWidget { ) : const SizedBox(), ), - if (Platform.isAndroid) - ListItemFactory.settingsItem( - key: AppKeys.upgrade_lantern_pro, - icon: ImagePaths.pro_icon_black, - content: 'Upgrade to Lantern Pro'.i18n, - onTap: () { - upgradeToLanternPro(context); - }, - ), + ListItemFactory.settingsItem( + key: AppKeys.upgrade_lantern_pro, + icon: ImagePaths.pro_icon_black, + content: 'Upgrade to Lantern Pro'.i18n, + onTap: () { + upgradeToLanternPro(context); + }, + ), ListItemFactory.settingsItem( icon: ImagePaths.star, content: 'Invite Friends'.i18n, @@ -80,23 +79,21 @@ class AccountMenu extends StatelessWidget { return [ messagingModel.getOnBoardingStatus( (context, hasBeenOnboarded, child) => - messagingModel.getCopiedRecoveryStatus( - (BuildContext context, bool hasCopiedRecoveryKey, Widget? child) => - ListItemFactory.settingsItem( - key: AppKeys.account_management, - icon: ImagePaths.account, - content: 'account_management'.i18n, - onTap: () async => await context - .pushRoute(AccountManagement(isPro: true)), - trailingArray: [ - if (!hasCopiedRecoveryKey && hasBeenOnboarded == true) - const CAssetImage( - path: ImagePaths.badge, - ), - ], - ) - - ), + messagingModel.getCopiedRecoveryStatus((BuildContext context, + bool hasCopiedRecoveryKey, Widget? child) => + ListItemFactory.settingsItem( + key: AppKeys.account_management, + icon: ImagePaths.account, + content: 'account_management'.i18n, + onTap: () async => + await context.pushRoute(AccountManagement(isPro: true)), + trailingArray: [ + if (!hasCopiedRecoveryKey && hasBeenOnboarded == true) + const CAssetImage( + path: ImagePaths.badge, + ), + ], + )), ), ListItemFactory.settingsItem( icon: ImagePaths.star, diff --git a/lib/account/account_management.dart b/lib/account/account_management.dart index fdeb2fb2f..cf3d4cf1f 100644 --- a/lib/account/account_management.dart +++ b/lib/account/account_management.dart @@ -2,7 +2,7 @@ import 'package:lantern/messaging/messaging.dart'; @RoutePage(name: 'AccountManagement') class AccountManagement extends StatefulWidget { - AccountManagement({Key? key, required this.isPro}) : super(key: key); + const AccountManagement({Key? key, required this.isPro}) : super(key: key); final bool isPro; @override @@ -257,7 +257,8 @@ class _AccountManagementState extends State }), ); - if (devices.devices.length < 3) { + // IOS does not support Link devices at the moment + if (devices.devices.length < 3&& Platform.isAndroid ) { proItems.add( ListItemFactory.settingsItem( content: '', diff --git a/lib/common/common.dart b/lib/common/common.dart index 64a1b3506..240bc8efa 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -101,3 +101,7 @@ export 'ui/custom/text_field.dart'; export 'ui/custom/fullscreen_video_viewer.dart'; export 'ui/custom/fullscreen_image_viewer.dart'; export 'ui/custom/fullscreen_viewer.dart'; + + +// Services +export 'package:lantern/core/services.dart'; \ No newline at end of file diff --git a/lib/common/session_model.dart b/lib/common/session_model.dart index b1e162063..78cf1d26c 100644 --- a/lib/common/session_model.dart +++ b/lib/common/session_model.dart @@ -149,6 +149,7 @@ class SessionModel extends Model { 'devices', builder: builder, deserialize: (Uint8List serialized) { + print("devices $serialized"); return Devices.fromBuffer(serialized); }, ); @@ -368,7 +369,9 @@ class SessionModel extends Model { return methodChannel.invokeMethod('redeemResellerCode', { 'email': email, 'resellerCode': resellerCode, - }).then((value) => value as String); + }).then((value) { + print("value $value"); + }); } Future submitBitcoinPayment( @@ -390,6 +393,14 @@ class SessionModel extends Model { }).then((value) => value as String); } + Future submitApplePlay(String planID, String purchaseToken) async { + return methodChannel + .invokeMethod('submitApplePayPayment', { + 'planID': planID, + 'purchaseId': purchaseToken, + }); + } + Future submitStripePayment( String planID, String email, diff --git a/lib/core/purchase/app_purchase.dart b/lib/core/purchase/app_purchase.dart new file mode 100644 index 000000000..ddf6f7f12 --- /dev/null +++ b/lib/core/purchase/app_purchase.dart @@ -0,0 +1,112 @@ +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:lantern/replica/common.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import '../../common/common.dart'; + +class AppPurchase { + final InAppPurchase _inAppPurchase = InAppPurchase.instance; + StreamSubscription>? _subscription; + List plansSku = []; + final Set _iosPlansIds = {"1Y", '2Y'}; + VoidCallback? _onSuccess; + Function(dynamic error)? _onError; + String _planId = ""; + + void init() { + final purchaseUpdated = _inAppPurchase.purchaseStream; + _subscription = purchaseUpdated.listen( + _onPurchaseUpdate, + onDone: _updateStreamOnDone, + onError: _updateStreamOnError, + ); + getAvailablePlans(); + } + + Future getAvailablePlans() async { + final response = await _inAppPurchase.queryProductDetails(_iosPlansIds); + plansSku.clear(); + plansSku.addAll(response.productDetails); + } + + Future startPurchase( + String planId, { + required VoidCallback onSuccess, + required Function(dynamic error) onFailure, + }) async { + _planId = planId; + _onSuccess = onSuccess; + _onError = onFailure; + final plan = _normalizePlan(planId); + final purchaseParam = PurchaseParam(productDetails: plan); + try { + await _inAppPurchase.buyConsumable(purchaseParam: purchaseParam); + } on PlatformException catch (e) { + logger.e('Error while calling purchase api', error: e); + Sentry.captureException(e); + _onError?.call(e); + } catch (e) { + logger.e('Payment failed', error: e); + _onError?.call(e); + } + } + + ProductDetails _normalizePlan(String planId) { + /// We have different ids for IOS, Android And servers + /// Convert Server plan to App Store plans + /// For ios we are using plans such as 1Y, 2Y, but server plan is 1y-xx-xx + /// So we split and compare with lowercase + final newPlanId = planId.split('-')[0]; + return plansSku.firstWhere( + (element) => element.id.toLowerCase() == newPlanId.toLowerCase(), + ); + } + + Future _onPurchaseUpdate( + List purchaseDetailsList, + ) async { + logger.i("_onPurchaseUpdate called with $purchaseDetailsList"); + for (var purchaseDetails in purchaseDetailsList) { + await _handlePurchase(purchaseDetails); + } + } + + Future _handlePurchase(PurchaseDetails purchaseDetails) async { + if (purchaseDetails.status == PurchaseStatus.canceled) { + // User has canceled the purchase + _onError?.call("Purchase canceled"); + return; + } + if (purchaseDetails.status == PurchaseStatus.purchased) { + try { + await sessionModel.submitApplePlay( + _planId, + purchaseDetails.verificationData.serverVerificationData, + ); + _onSuccess?.call(); + } catch (e) { + logger.e("purchase error", error: e); + Sentry.captureException(e); + _onError?.call(e); + } + } + if (purchaseDetails.pendingCompletePurchase) { + await _inAppPurchase.completePurchase(purchaseDetails); + } + } + + void _updateStreamOnDone() { + _onError = null; + _onSuccess = null; + _planId = ""; + _subscription?.cancel(); + } + + void _updateStreamOnError(dynamic error) { + //Handle error here + logger.e("purchase error", error: error); + if (_onError != null) { + _onError?.call(error); + } + } +} diff --git a/lib/core/services.dart b/lib/core/services.dart new file mode 100644 index 000000000..e5ac2f5b2 --- /dev/null +++ b/lib/core/services.dart @@ -0,0 +1,9 @@ +import 'package:get_it/get_it.dart'; +import 'package:lantern/core/purchase/app_purchase.dart'; + +final GetIt sl = GetIt.instance; + +void init() { + //Inject + sl.registerLazySingleton(() => AppPurchase()); +} diff --git a/lib/main.dart b/lib/main.dart index 55337f4eb..1e1d4388e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,9 @@ import 'package:flutter_driver/driver_extension.dart'; import 'package:google_mobile_ads/google_mobile_ads.dart'; import 'package:lantern/app.dart'; import 'package:lantern/common/common.dart'; +import 'package:lantern/core/purchase/app_purchase.dart'; +import 'package:lantern/core/services.dart'; + import 'catcher_setup.dart'; Future main() async { @@ -12,8 +15,10 @@ Future main() async { print("Flutter extension enabled $flavor"); enableFlutterDriverExtension(); } - WidgetsFlutterBinding.ensureInitialized(); + // Inject all the services + init(); + sl().init(); await _initGoogleMobileAds(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); diff --git a/lib/plans/plan_details.dart b/lib/plans/plan_details.dart index 51654359a..16474a359 100644 --- a/lib/plans/plan_details.dart +++ b/lib/plans/plan_details.dart @@ -1,4 +1,6 @@ import 'package:lantern/common/common.dart'; +import 'package:lantern/core/purchase/app_purchase.dart'; +import 'package:lantern/plans/utils.dart'; class PlanCard extends StatelessWidget { final Plan plan; @@ -20,34 +22,7 @@ class PlanCard extends StatelessWidget { return Padding( padding: const EdgeInsetsDirectional.only(bottom: 16.0), child: CInkWell( - onTap: () async { - final isPlayVersion = sessionModel.isPlayVersion.value ?? false; - final inRussia = sessionModel.country.value == 'RU'; - - // * Play version - if (isPlayVersion && !inRussia) { - await sessionModel - .submitGooglePlay(planName) - .onError((error, stackTrace) { - // on failure - CDialog.showError( - context, - error: e, - stackTrace: stackTrace, - description: - (error as PlatformException).message ?? error.toString(), - ); - }); - } else { - // * Proceed to our own Checkout - await context.pushRoute( - Checkout( - plan: plan, - isPro: isPro, - ), - ); - } - }, + onTap: () => onPlanTap(context, planName), child: Stack( alignment: Alignment.bottomCenter, children: [ @@ -145,6 +120,75 @@ class PlanCard extends StatelessWidget { ), ); } + + Future onPlanTap(BuildContext context, String planName) async { + if (Platform.isAndroid) { + _proceedToAndroidCheckout(context, planName); + } else { + _proceedToCheckoutIOS(context); + } + } + + Future _proceedToAndroidCheckout( + BuildContext context, String planName) async { + final isPlayVersion = sessionModel.isPlayVersion.value ?? false; + final inRussia = sessionModel.country.value == 'RU'; + //If user is downloaded from Play store and !inRussia then + //Go with In App purchase + if (isPlayVersion && !inRussia) { + await sessionModel + .submitGooglePlay(planName) + .onError((error, stackTrace) { + // on failure + CDialog.showError( + context, + error: e, + stackTrace: stackTrace, + description: (error as PlatformException).message ?? error.toString(), + ); + }); + } else { + _proceedToCustomCheckout(context); + } + } + + Future _proceedToCustomCheckout(BuildContext context) async { + await context.pushRoute( + Checkout( + plan: plan, + isPro: isPro, + ), + ); + } + + void _proceedToCheckoutIOS(BuildContext context) { + final appPurchase = sl(); + try { + context.loaderOverlay.show(); + appPurchase.startPurchase( + plan.id, + onSuccess: () { + context.loaderOverlay.hide(); + showSuccessDialog(context, isPro); + }, + onFailure: (error) { + context.loaderOverlay.hide(); + CDialog.showError( + context, + error: error, + description: error.toString(), + ); + }, + ); + } catch (e) { + context.loaderOverlay.hide(); + CDialog.showError( + context, + error: e, + description: e.toString(), + ); + } + } } class PlanStep extends StatelessWidget { diff --git a/lib/plans/plans.dart b/lib/plans/plans.dart index 6ab9f3268..b178d3532 100644 --- a/lib/plans/plans.dart +++ b/lib/plans/plans.dart @@ -64,11 +64,9 @@ class PlansPage extends StatelessWidget { color: white, child: Row( children: [ - Container( - child: const CAssetImage( - path: ImagePaths.lantern_pro_logotype, - size: 20, - ), + const CAssetImage( + path: ImagePaths.lantern_pro_logotype, + size: 20, ), const Spacer(), IconButton( diff --git a/lib/plans/reseller_checkout.dart b/lib/plans/reseller_checkout.dart index 19d1ca04f..c40e9377f 100644 --- a/lib/plans/reseller_checkout.dart +++ b/lib/plans/reseller_checkout.dart @@ -36,7 +36,7 @@ class ResellerCodeFormatter extends TextInputFormatter { class ResellerCodeCheckout extends StatefulWidget { final bool isPro; - ResellerCodeCheckout({ + const ResellerCodeCheckout({ required this.isPro, Key? key, }) : super(key: key); @@ -51,7 +51,7 @@ class _ResellerCodeCheckoutState extends State { formKey: emailFieldKey, validator: (value) => EmailValidator.validate(value ?? '') ? null - : 'please_enter_a_valid_email_address'.i18n, + : 'please_entera_valid_email_address'.i18n, ); final resellerCodeFieldKey = GlobalKey(); @@ -156,35 +156,7 @@ class _ResellerCodeCheckoutState extends State { emailFieldKey.currentState?.validate() == false || resellerCodeFieldKey.currentState?.validate() == false, text: copy, - onPressed: () async { - context.loaderOverlay.show(); - await sessionModel - .redeemResellerCode( - emailController.text, - resellerCodeController.text, - ) - .timeout( - defaultTimeoutDuration, - onTimeout: () => onAPIcallTimeout( - code: 'redeemresellerCodeTimeout', - message: 'reseller_timeout'.i18n, - ), - ) - .then((value) { - context.loaderOverlay.hide(); - showSuccessDialog(context, widget.isPro, true); - }).onError((error, stackTrace) { - context.loaderOverlay.hide(); - CDialog.showError( - context, - error: e, - stackTrace: stackTrace, - description: (error as PlatformException) - .message - .toString(), // This is coming localized - ); - }); - }, + onPressed: onRegisterTap, ), ], ) @@ -194,4 +166,34 @@ class _ResellerCodeCheckoutState extends State { ); }); } + + Future onRegisterTap() async { + context.loaderOverlay.show(); + try { + await sessionModel + .redeemResellerCode( + emailController.text, + resellerCodeController.text, + ).timeout( + defaultTimeoutDuration, + onTimeout: () => onAPIcallTimeout( + code: 'redeemresellerCodeTimeout', + message: 'reseller_timeout'.i18n, + ), + ); + + context.loaderOverlay.hide(); + showSuccessDialog(context, widget.isPro, true); + } catch (error,s) { + context.loaderOverlay.hide(); + CDialog.showError( + context, + error: e, + stackTrace: s, + description: (error as PlatformException) + .message + .toString(), // This is coming localized + ); + } + } } diff --git a/lib/plans/utils.dart b/lib/plans/utils.dart index 204213adf..f1d4ff644 100644 --- a/lib/plans/utils.dart +++ b/lib/plans/utils.dart @@ -33,7 +33,9 @@ void showSuccessDialog(BuildContext context, bool isPro, [bool? isReseller]) { description: description, actionLabel: 'continue_to_pro'.i18n, agreeAction: () async { - await context.pushRoute(Home()); + // Note: whatever page you need to popUtil + // it will pop that page + context.router.popUntil((route) => route.settings.name == PlansPage.name); return true; }, ); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d3a76df8c..09998cfdf 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import device_info_plus import emoji_picker_flutter import flutter_inappwebview import flutter_local_notifications +import in_app_purchase_storekit import package_info_plus import path_provider_foundation import sentry_flutter @@ -25,6 +26,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + InAppPurchasePlugin.register(with: registry.registrar(forPlugin: "InAppPurchasePlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) diff --git a/protos_shared/vpn.proto b/protos_shared/vpn.proto index c6d3a7bdc..89a1e2266 100644 --- a/protos_shared/vpn.proto +++ b/protos_shared/vpn.proto @@ -32,6 +32,10 @@ message Devices { repeated Device devices = 1; } +message Plans { + repeated Plan plan = 1; +} + message Plan { string id = 1; string description = 2; diff --git a/pubspec.lock b/pubspec.lock index 1ee8ef8f6..7bd4833b9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -671,6 +671,14 @@ packages: description: flutter source: sdk version: "0.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3 + url: "https://pub.dev" + source: hosted + version: "7.6.4" gettext_parser: dependency: transitive description: @@ -759,6 +767,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + in_app_purchase: + dependency: "direct main" + description: + name: in_app_purchase + sha256: bdda02b5b11b56d5e29c7f0c57c433db3452b0c8ce1c37cbfcf1de52946efd9f + url: "https://pub.dev" + source: hosted + version: "3.1.11" + in_app_purchase_android: + dependency: transitive + description: + name: in_app_purchase_android + sha256: c4b84caa4e2c7ffebda444c5033fd8423cc3a45a6e1066929bbbcd4daf665db5 + url: "https://pub.dev" + source: hosted + version: "0.3.0+15" + in_app_purchase_platform_interface: + dependency: transitive + description: + name: in_app_purchase_platform_interface + sha256: "5168afbc54f406f741252b66d41872c1193a0066a6edcb587176290b92e2d537" + url: "https://pub.dev" + source: hosted + version: "1.3.6" + in_app_purchase_storekit: + dependency: transitive + description: + name: in_app_purchase_storekit + sha256: "88afd256c7605d431f0ce29d0161f9554851f90ecb92ceb9e18196c4e7858d52" + url: "https://pub.dev" + source: hosted + version: "0.3.6+7" infinite_scroll_pagination: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d6074d8d7..d8da8667e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -112,10 +112,16 @@ dependencies: permission_handler: ^10.4.3 flutter_markdown: ^0.6.17+1 + # Purchase + in_app_purchase: ^3.1.11 + # Ads google_mobile_ads: ^3.0.0 # clever_ads_solutions: ^0.1.0 + # Service Locator + get_it: ^7.6.4 + # wakelock ^0.6.2 requires win32 ^2.0.0 or ^3.0.0 # See https://github.com/creativecreatorormaybenot/wakelock/issues/211