Created
October 30, 2021 04:27
-
-
Save while-loop/53807702379605bfc23680a0b45a2043 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"context" | |
"encoding/json" | |
"fmt" | |
"github.com/google/uuid" | |
"github.com/mhemmings/revenuecat" | |
"github.com/while-loop/dasbudget-api/internal/app" | |
"github.com/while-loop/dasbudget-api/internal/auth" | |
"github.com/while-loop/dasbudget-api/internal/config" | |
"github.com/while-loop/dasbudget-api/internal/config/dsub" | |
"github.com/while-loop/dasbudget-api/internal/log" | |
_ "github.com/while-loop/dasbudget-api/internal/provider/monzo" | |
_ "github.com/while-loop/dasbudget-api/internal/provider/mx" | |
_ "github.com/while-loop/dasbudget-api/internal/provider/plaid" | |
"google.golang.org/api/androidpublisher/v3" | |
"google.golang.org/api/option" | |
"io" | |
"io/ioutil" | |
"net/http" | |
"os" | |
"time" | |
) | |
var ( | |
appleAppID = "" | |
appleSubGroupID = "subscription group ID (family group ID)" | |
appleHost = "https://appstoreconnect.apple.com" | |
appleClient = &http.Client{Timeout: 30 * time.Second} | |
appleCookie = `copy from chrome inspect` | |
priceTiers = map[float64]string{ | |
3.99: "8", | |
4.49: "9", | |
4.99: "10", | |
5.49: "11", | |
5.99: "12", | |
6.49: "13", | |
6.99: "14", | |
7.49: "15", | |
7.99: "16", | |
8.49: "17", | |
8.99: "18", | |
9.49: "19", | |
9.99: "20", | |
10.49: "21", | |
10.99: "22", | |
11.49: "23", | |
39.99: "70", | |
44.99: "75", | |
49.99: "80", | |
54.99: "85", | |
59.99: "90", | |
64.99: "95", | |
69.99: "100", | |
74.99: "105", | |
79.99: "110", | |
84.99: "115", | |
89.99: "120", | |
94.99: "125", | |
99.99: "130", | |
104.99: "135", | |
109.99: "140", | |
114.99: "145", | |
} | |
priceReqs = map[string]*SetPriceReq{} | |
CreateSubscriptionUrl = "%s/WebObjects/iTunesConnect.woa/ra/apps/%s/iaps" | |
SetPricingUrl = "%s/WebObjects/iTunesConnect.woa/ra/apps/%s/iaps/%s/pricing/subscriptions" | |
GetPricingUrl = "%s/WebObjects/iTunesConnect.woa/ra/apps/%s/iaps/%s/pricing/equalize/USD/%s" | |
GetIAPsUrl = "%s/WebObjects/iTunesConnect.woa/ra/apps/%s/iaps" | |
GetIAPUrl = "%s/WebObjects/iTunesConnect.woa/ra/apps/%s/iaps/%s" | |
) | |
func main() { | |
noerr := func(err error) { | |
if err != nil { | |
panic(err) | |
} | |
} | |
ctx := context.Background() | |
noerr(doIOS(ctx, noerr)) | |
} | |
func doIOS(ctx context.Context, noerr func(e error)) error { | |
var appleRsp *AppleResp | |
appleIAPs, err := getIAPs(ctx) | |
if err != nil { | |
return err | |
} | |
fmt.Println(len(appleIAPs), "Apple Products") | |
for _, iap := range appleIAPs { | |
fmt.Println(iap.VendorId) | |
} | |
p := dsub.Product{ | |
ID: "test_db_399_1m_1acc_standard_30d0_ios", | |
OfferingID: "default", | |
Tier: dsub.EntStandard, | |
AccountLimit: 4, | |
Accounts: dsub.Ent4Account, | |
Frequency: dsub.FreqMonthly, | |
Platform: "ios", | |
Price: 3.99, | |
Sandbox: true, | |
} | |
log.Infof("creating sub %s", p.ID) | |
req, err := createSub(ctx, p, noerr, appleRsp) | |
log.Infof("finding sub ID %s", p.ID) | |
iap, err := waitForIAP(ctx, p.ID) | |
noerr(err) | |
log.Infof("uploading image to sub %s %s", iap.ProductID.Value, iap.AdamID) | |
noerr(uploadScreenshot(ctx, iap)) | |
log.Infof("getting prices for sub %s %s", p.ID, iap.AdamID) | |
req, err = http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf(SetPricingUrl, appleHost, appleAppID, iap.AdamID), bod(pricingBody(iap.AdamID, p.Price))) | |
noerr(err) | |
log.Infof("setting prices for sub %s", p.ID) | |
noerr(do(ctx, req, &appleRsp)) | |
mustSucceed(appleRsp, "failed to set price for sku %s %v", p.ID, p.Price) | |
log.Infof("prices set for sub %s", p.ID) | |
// todo, set introductory price | |
return nil | |
} | |
func createSub(ctx context.Context, p dsub.Product, noerr func(e error), appleRsp *AppleResp) (*http.Request, error) { | |
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf(CreateSubscriptionUrl, appleHost, appleAppID), bod(CreateSubscriptionReq{ | |
SectionErrorKeys: []interface{}{}, | |
SectionInfoKeys: []interface{}{}, | |
SectionWarningKeys: []interface{}{}, | |
FamilyID: appleSubGroupID, | |
AddOnType: "recurring", | |
IsNewsSubscription: false, | |
IsReplaced: false, | |
ReplacementAdamID: nil, | |
ReferenceName: ValueObj{ | |
Value: p.ID, | |
IsEditable: true, | |
IsRequired: true, | |
MaxLength: 64, | |
MinLength: 2, | |
}, | |
ProductID: ValueObj{ | |
Value: p.ID, | |
IsEditable: true, | |
IsRequired: true, | |
MaxLength: 100, | |
MinLength: 2, | |
}, | |
ClearedForSale: ValueObj{ | |
Value: true, | |
IsEditable: true, | |
IsRequired: false, | |
}, | |
PricingDurationType: ValueObj{ | |
Value: p.AppleDuration(), // "1m", "1y" | |
IsEditable: true, | |
IsRequired: false, | |
}, | |
PricingIntervals: []interface{}{}, | |
FreeTrialDurationType: ValueObj{ | |
Value: nil, | |
IsEditable: true, | |
IsRequired: false, | |
}, | |
BonusPeriodDurationType: ValueObj{ | |
Value: nil, | |
IsEditable: true, | |
IsRequired: false, | |
}, | |
MissingRequiredPrivacyPolicyData: false, | |
MissingRequiredFamilyDetail: false, | |
FamilyShareable: nil, | |
Versions: []Version{ | |
{ | |
ID: nil, | |
Details: ValueObj{ | |
Value: []ValueObj{ | |
{ | |
Value: VersionDetails{ | |
Name: ValueObj{ | |
Value: p.Title(), //friendly subscription name | |
IsEditable: true, | |
IsRequired: true, | |
MaxLength: 30, | |
MinLength: 2, | |
}, | |
Description: ValueObj{ | |
Value: p.Description(), //sub description | |
IsEditable: true, | |
IsRequired: true, | |
MaxLength: 45, | |
MinLength: 1, | |
}, | |
LocaleCode: "en-US", | |
}, | |
IsEditable: false, | |
IsRequired: false, | |
ErrorKeys: nil, | |
IsDeletable: true, | |
}, | |
}, | |
IsEditable: true, | |
IsRequired: false, | |
}, | |
ContentHosting: nil, | |
ContentHostingData: nil, | |
ReviewNotes: ValueObj{ | |
Value: "DAS Budget provides 2 types of subscriptions in order to fully use the app.\n" + | |
"The first is the Standard tier that grants basic and features needed to use the app in its full form. That includes:\n" + | |
"- Ability to link their bank accounts\n" + | |
"- Ability to get 1 to 6 updates a day\n" + | |
"\n" + | |
"\n" + | |
"Premium subscription is the higher tier and unlocks better quality of life features for the user including:\n" + | |
"- Ability to fetch new data from bank accounts on-demand\n" + | |
"- Ability to link their credit card transactions into their budgets\n" + | |
"- Ability to round up transactions to the nearest dollar amount and deposit that into goals\n", | |
IsEditable: true, | |
IsRequired: false, | |
MaxLength: 4000, | |
MinLength: 2, | |
}, | |
ReviewScreenshot: ValueObj{ | |
Value: RevScreenValue{ | |
Size: 0, | |
Width: 0, | |
Height: 0, | |
Checksum: "", | |
AssetToken: "", | |
SortOrder: 0, | |
OriginalFileName: "", | |
Type: "", | |
URL: "", | |
ThumbNailURL: "", | |
}, | |
IsEditable: true, | |
IsRequired: false, | |
}, | |
Merch: MerchField{ | |
Images: []MerchImage{ | |
{ | |
ID: nil, | |
Status: nil, | |
Image: ValueObj{ | |
Value: ImgValue{ | |
AssetToken: nil, | |
OriginalFileName: nil, | |
Width: nil, | |
Height: nil, | |
Checksum: nil, | |
}, | |
IsEditable: true, | |
IsRequired: false, | |
}, | |
}, | |
}, | |
ShowByDefault: true, | |
IsActive: false, | |
}, | |
Status: nil, | |
CanBeSubmitted: false, | |
}, | |
}, | |
})) | |
noerr(err) | |
noerr(do(ctx, req, &appleRsp)) | |
mustSucceed(appleRsp, "failed to create sub %s %v", p.ID, p.Price) | |
return err | |
} | |
func uploadScreenshot(ctx context.Context, iap *CreateSubscriptionReq) error { | |
specs := upload(ctx) | |
time.Sleep(5 * time.Second) | |
iap.Versions[0].ReviewScreenshot = ValueObj{ | |
Value: RevScreenValue{ | |
Size: specs.Length, | |
Width: specs.Width, | |
Height: specs.Height, | |
Checksum: specs.Md5, | |
AssetToken: specs.Token, | |
OriginalFileName: "IMG_0059.PNG", // name of img | |
Type: "MZPFT.SortedD22ScreenShot", | |
URL: "", | |
ThumbNailURL: "", | |
SortOrder: 0, | |
}, | |
IsEditable: true, | |
IsRequired: false, | |
} | |
var appleRsp *AppleResp | |
req, err := http.NewRequestWithContext(ctx, http.MethodPut, fmt.Sprintf(GetIAPUrl, appleHost, appleAppID, iap.AdamID), bod(iap)) | |
if err != nil { | |
return err | |
} | |
if err = do(ctx, req, &appleRsp); err != nil { | |
return err | |
} | |
mustSucceed(appleRsp, "failed to upload sub image %s %v", iap.ProductID, iap.AdamID) | |
return nil | |
} | |
func upload(ctx context.Context) *UploadImageResp { | |
p := "/home/anthony/Downloads/IMG_0059.PNG" // path to file | |
f, err := os.Open(p) | |
if err != nil { | |
panic(fmt.Sprintf("failed to open screenshot %s: %v", p, err)) | |
} | |
defer f.Close() | |
rbs, err := ioutil.ReadAll(f) | |
if err != nil { | |
panic(fmt.Sprintf("failed to open screenshot %s: %v", p, err)) | |
} | |
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://du-itc.itunes.apple.com/upload/image", bytes.NewReader(rbs)) | |
if err != nil { | |
panic(fmt.Sprintf("failed to create screenshot post: %v", err)) | |
} | |
req.Header.Add("Accept", "application/json, text/plain, */*") | |
req.Header.Add("Content-Type", "image/png") | |
req.Header.Add("Content-length", fmt.Sprint(len(rbs))) | |
req.Header.Add("X-Apple-Upload-Referrer", fmt.Sprintf("https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app/%s/addons/%s", appleAppID, "APPLE SUB ID")) | |
req.Header.Add("Referrer", "https://appstoreconnect.apple.com/") | |
req.Header.Add("X-Apple-Request-UUID", uuid.New().String()) | |
req.Header.Add("X-Apple-Upload-AppleId", appleAppID) | |
req.Header.Add("X-Apple-Upload-itctoken", "COPY FROM CHROME INSPECT") | |
req.Header.Add("X-Apple-Upload-ContentProviderId", "COPY FROM CHROME INSPECT") | |
req.Header.Add("X-Original-Filename", "IMG_0059.PNG") // todo, get file name here | |
req.Header.Add("X-Apple-Upload-Validation-RuleSets", "MZPFT.SortedD22ScreenShot") | |
req.Header.Add("Connection", "keep-alive") | |
req.Header.Add("cookie", appleCookie) | |
rsp, err := appleClient.Do(req) | |
if err != nil { | |
panic(fmt.Sprintf("failed to do screenshot post: %v", err)) | |
} | |
defer rsp.Body.Close() | |
bs, err := ioutil.ReadAll(rsp.Body) | |
if err != nil { | |
panic(fmt.Sprintf("failed to do read all screenshot post: %v", err)) | |
} | |
if rsp.StatusCode != 201 { | |
panic(fmt.Sprintf("failed to do not 201 screenshot post %s", string(bs))) | |
} | |
var resp *UploadImageResp | |
if err = json.Unmarshal(bs, &resp); err != nil { | |
panic(fmt.Sprintf("failed to do unmarshal screenshot post: %v", err)) | |
} | |
return resp | |
} | |
func pricingBody(subAdamID string, f float64) *SetPriceReq { | |
tier := priceTiers[f] | |
if tier == "" { | |
panic(fmt.Errorf("unknown tier %f", f)) | |
} | |
req, ok := priceReqs[tier] | |
if !ok { | |
req = getTierPricing(context.Background(), subAdamID, tier) | |
priceReqs[tier] = req | |
} | |
return req | |
} | |
func bod(in interface{}) io.Reader { | |
js, err := json.Marshal(in) | |
if err != nil { | |
panic(fmt.Errorf("error marshaling request body: %v", err)) | |
} | |
return bytes.NewBuffer(js) | |
} | |
func do(ctx context.Context, r *http.Request, out interface{}) error { | |
r.Header.Add("Authority", "appstoreconnect.apple.com") | |
r.Header.Add("Method", r.Method) | |
r.Header.Add("Path", r.URL.Path) | |
r.Header.Add("Scheme", r.URL.Scheme) | |
r.Header.Add("Accept", "application/json, text/plain, */*") | |
r.Header.Add("Accept-Language", "en-US,en;q=0.9") | |
r.Header.Add("Content-Type", "application/json;charset=UTF-8") | |
r.Header.Add("cookie", appleCookie) | |
r.Header.Add("origin", "https://appstoreconnect.apple.com") | |
r.Header.Add("referer", fmt.Sprintf("https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/ng/app/%s/addons", appleAppID)) | |
r.Header.Add("sec-ch-ua", `"Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99"`) | |
r.Header.Add("sec-ch-ua-mobile", "?0") | |
r.Header.Add("sec-ch-ua-platform", `"Linux"`) | |
r.Header.Add("sec-fetch-dest", "empty") | |
r.Header.Add("sec-fetch-mode", "cors") | |
r.Header.Add("sec-fetch-site", "same-origin") | |
r.Header.Add("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.81 Safari/537.36") | |
r.Header.Add("x-csrf-itc", "itc") | |
rsp, err := appleClient.Do(r) | |
if err != nil { | |
return err | |
} | |
defer rsp.Body.Close() | |
bs, err := ioutil.ReadAll(rsp.Body) | |
if err != nil { | |
return err | |
} | |
if rsp.StatusCode >= 300 { | |
return fmt.Errorf("apple error: %d %s\n\n%s", rsp.StatusCode, rsp.Status, bs) | |
} else if out != nil { | |
return json.Unmarshal(bs, out) | |
} | |
return nil | |
} | |
func getTierPricing(ctx context.Context, subAdamID, tier string) *SetPriceReq { | |
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(GetPricingUrl, appleHost, appleAppID, subAdamID, tier), nil) | |
if err != nil { | |
panic("failed to get price req " + err.Error()) | |
} | |
var rsp *PricingResp | |
if err := do(ctx, req, &rsp); err != nil { | |
panic("failed to get prices" + err.Error()) | |
} | |
sReq := &SetPriceReq{Subscriptions: nil} | |
for countryAlpha2, info := range rsp.Data { | |
sReq.Subscriptions = append(sReq.Subscriptions, ValueObj{ | |
Value: AppSubValue{ | |
Country: countryAlpha2, | |
Grandfathered: ValueObj{ | |
Value: "FUTURE_NONE", | |
IsEditable: false, | |
IsRequired: false, | |
}, | |
PriceTierEffectiveDate: nil, | |
PriceTierEndDate: nil, | |
TierStem: info.TierStem, | |
}, | |
IsEditable: true, | |
IsRequired: false, | |
}) | |
} | |
return sReq | |
} | |
func waitForIAP(ctx context.Context, sku string) (*CreateSubscriptionReq, error) { | |
for i := 0; i < 60; i++ { | |
time.Sleep(1 * time.Second) | |
iaps, err := getIAPs(ctx) | |
if err != nil { | |
return nil, err | |
} | |
for _, iap := range iaps { | |
if iap.VendorId == sku { | |
return getIAP(ctx, iap.AdamId) | |
} | |
} | |
} | |
return nil, fmt.Errorf("timeout for sku ID") | |
} | |
func getIAP(ctx context.Context, subAdamID string) (*CreateSubscriptionReq, error) { | |
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(GetIAPUrl, appleHost, appleAppID, subAdamID), nil) | |
if err != nil { | |
return nil, err | |
} | |
var rsp *IAPResp | |
if err := do(ctx, req, &rsp); err != nil { | |
return nil, err | |
} | |
return rsp.Data, nil | |
} | |
func getIAPs(ctx context.Context) ([]*IAPData, error) { | |
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf(GetIAPsUrl, appleHost, appleAppID), nil) | |
if err != nil { | |
return nil, err | |
} | |
var rsp *IAPsResp | |
if err := do(ctx, req, &rsp); err != nil { | |
return nil, err | |
} | |
return rsp.Data, nil | |
} | |
func mustSucceed(rsp *AppleResp, format string, args ...interface{}) { | |
if rsp.StatusCode != "SUCCESS" { | |
msg := fmt.Sprintf(format, args...) | |
panic(fmt.Sprintf("%s: %v", msg, rsp)) | |
} | |
} | |
type ( | |
CreateSubscriptionReq struct { | |
SectionErrorKeys []interface{} `json:"sectionErrorKeys"` | |
SectionInfoKeys []interface{} `json:"sectionInfoKeys"` | |
SectionWarningKeys []interface{} `json:"sectionWarningKeys"` | |
Value interface{} `json:"value"` | |
ID interface{} `json:"id"` | |
AdamID string `json:"adamId,omitempty"` | |
PublicID string `json:"publicId,omitempty"` | |
AppAdamIds []string `json:"appAdamIds,omitempty"` | |
FamilyID string `json:"familyId"` | |
AddOnType string `json:"addOnType"` | |
IsNewsSubscription bool `json:"isNewsSubscription"` | |
IsReplaced bool `json:"isReplaced"` | |
ReplacementAdamID interface{} `json:"replacementAdamId"` | |
ReferenceName ValueObj `json:"referenceName"` | |
ProductID ValueObj `json:"productId"` | |
ClearedForSale ValueObj `json:"clearedForSale"` | |
PricingDurationType ValueObj `json:"pricingDurationType"` | |
PricingIntervals []interface{} `json:"pricingIntervals"` | |
UngrandfatheredIntervals interface{} `json:"ungrandfatheredIntervals"` | |
FreeTrialDurationType ValueObj `json:"freeTrialDurationType"` | |
BonusPeriodDurationType ValueObj `json:"bonusPeriodDurationType"` | |
Versions []Version `json:"versions"` | |
MissingRequiredPrivacyPolicyData bool `json:"missingRequiredPrivacyPolicyData"` | |
MissingRequiredFamilyDetail bool `json:"missingRequiredFamilyDetail"` | |
FamilyShareable interface{} `json:"familyShareable"` | |
} | |
ValueObj struct { | |
Value interface{} `json:"value"` | |
IsEditable bool `json:"isEditable"` | |
IsRequired bool `json:"isRequired"` | |
IsDeletable bool `json:"isDeletable"` | |
ErrorKeys interface{} `json:"errorKeys"` | |
MaxLength int `json:"maxLength,omitempty"` | |
MinLength int `json:"minLength,omitempty"` | |
} | |
RevScreenValue struct { | |
Size int `json:"size,omitempty"` | |
Width int `json:"width,omitempty"` | |
Height int `json:"height,omitempty"` | |
Checksum string `json:"checksum,omitempty"` | |
AssetToken string `json:"assetToken,omitempty"` | |
SortOrder int `json:"sortOrder,omitempty"` | |
OriginalFileName string `json:"originalFileName,omitempty"` | |
Type string `json:"type,omitempty"` | |
URL string `json:"url,omitempty"` | |
ThumbNailURL string `json:"thumbNailUrl,omitempty"` | |
} | |
ImgValue struct { | |
AssetToken interface{} `json:"assetToken"` | |
OriginalFileName interface{} `json:"originalFileName"` | |
Width interface{} `json:"width"` | |
Height interface{} `json:"height"` | |
Checksum interface{} `json:"checksum"` | |
} | |
Version struct { | |
ID interface{} `json:"id"` | |
Details ValueObj `json:"details"` | |
ContentHosting interface{} `json:"contentHosting"` | |
ContentHostingData interface{} `json:"contentHostingData"` | |
ReviewNotes ValueObj `json:"reviewNotes"` | |
ReviewScreenshot ValueObj `json:"reviewScreenshot"` | |
Merch MerchField `json:"merch"` | |
Status interface{} `json:"status"` | |
CanBeSubmitted bool `json:"canBeSubmitted"` | |
} | |
VersionDetails struct { | |
ID interface{} `json:"id"` | |
Name ValueObj `json:"name"` | |
Description ValueObj `json:"description"` | |
PublicationName interface{} `json:"publicationName"` | |
LocaleCode string `json:"localeCode"` | |
Status interface{} `json:"status"` | |
ErrorKeys interface{} `json:"errorKeys"` | |
} | |
MerchField struct { | |
Images []MerchImage `json:"images"` | |
ShowByDefault bool `json:"showByDefault"` | |
IsActive bool `json:"isActive"` | |
} | |
MerchImage struct { | |
ID interface{} `json:"id"` | |
Image ValueObj `json:"image"` | |
Status interface{} `json:"status"` | |
} | |
AppleResp struct { | |
Data interface{} `json:"data"` | |
Messages AppleMsg `json:"messages"` | |
StatusCode string `json:"statusCode"` | |
} | |
PricingResp struct { | |
Data map[string]PriceInfo `json:"data"` | |
AppleResp | |
} | |
IAPsResp struct { | |
Data []*IAPData `json:"data"` | |
AppleResp | |
} | |
IAPResp struct { | |
Data *CreateSubscriptionReq `json:"data"` | |
AppleResp | |
} | |
IAPData struct { | |
FamilyReferenceName string `json:"familyReferenceName"` | |
DurationDays int `json:"durationDays"` | |
NumberOfCodes int `json:"numberOfCodes"` | |
MaximumNumberOfCodes int `json:"maximumNumberOfCodes"` | |
AppMaximumNumberOfCodes int `json:"appMaximumNumberOfCodes"` | |
IsEditable bool `json:"isEditable"` | |
IsRequired bool `json:"isRequired"` | |
CanDeleteAddOn bool `json:"canDeleteAddOn"` | |
ErrorKeys interface{} `json:"errorKeys"` | |
IsEmptyValue bool `json:"isEmptyValue"` | |
ItcsubmitNextVersion bool `json:"itcsubmitNextVersion"` | |
AdamId string `json:"adamId"` | |
ReferenceName string `json:"referenceName"` | |
VendorId string `json:"vendorId"` | |
AddOnType string `json:"addOnType"` | |
Versions []struct { | |
ScreenshotUrl interface{} `json:"screenshotUrl"` | |
CanSubmit bool `json:"canSubmit"` | |
IssuesCount int `json:"issuesCount"` | |
ItunesConnectStatus string `json:"itunesConnectStatus"` | |
} `json:"versions"` | |
PurpleSoftwareAdamIds []string `json:"purpleSoftwareAdamIds"` | |
LastModifiedDate int64 `json:"lastModifiedDate"` | |
IsNewsSubscription bool `json:"isNewsSubscription"` | |
ITunesConnectStatus string `json:"iTunesConnectStatus"` | |
} | |
PriceInfo struct { | |
CountryName string `json:"countryName"` | |
FRetailPrice string `json:"fRetailPrice"` | |
FWholesalePrice string `json:"fWholesalePrice"` | |
FWholesalePrice2 string `json:"fWholesalePrice2"` | |
TierStem string `json:"tierStem"` | |
} | |
AppleMsg struct { | |
Warn []string `json:"warn"` | |
Error []string `json:"error"` | |
Info []string `json:"info"` | |
} | |
SetPriceReq struct { | |
Subscriptions []ValueObj `json:"subscriptions"` | |
} | |
AppSubValue struct { | |
Country string `json:"country"` | |
Grandfathered ValueObj `json:"grandfathered"` | |
PriceTierEffectiveDate interface{} `json:"priceTierEffectiveDate"` | |
PriceTierEndDate interface{} `json:"priceTierEndDate"` | |
TierStem string `json:"tierStem"` | |
} | |
UploadImageResp struct { | |
ContentType string `json:"contentType"` | |
DescriptionDoc interface{} `json:"descriptionDoc"` | |
DsID interface{} `json:"dsId"` | |
FileType interface{} `json:"fileType"` | |
HasAlpha bool `json:"hasAlpha"` | |
Height int `json:"height"` | |
ImgSvcTemplateURL interface{} `json:"imgSvcTemplateUrl"` | |
Length int `json:"length"` | |
LocalizedMessage interface{} `json:"localizedMessage"` | |
Md5 string `json:"md5"` | |
NonLocalizedMessage interface{} `json:"nonLocalizedMessage"` | |
SuggestionCode interface{} `json:"suggestionCode"` | |
Token string `json:"token"` | |
TokenType string `json:"tokenType"` | |
Type string `json:"type"` | |
WarningCodes interface{} `json:"warningCodes"` | |
Width int `json:"width"` | |
} | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment