Printful Integration for WooCommerce - Version 2.1.32

Version Description

Woocommerce compatibility raised to 6.5.1 WordPress compatibility raised to 6.0

Download this release

Release Info

Developer printful
Plugin Icon 128x128 Printful Integration for WooCommerce
Version 2.1.32
Comparing to
See all releases

Code changes from version 2.1.31 to 2.1.32

Files changed (56) hide show
  1. printful-shipping.php +1 -1
  2. readme.txt +10 -2
  3. trunk/assets/css/dashboard.css +181 -0
  4. trunk/assets/css/global.css +19 -0
  5. trunk/assets/css/settings.css +46 -0
  6. trunk/assets/css/size-guide.css +169 -0
  7. trunk/assets/css/status.css +21 -0
  8. trunk/assets/css/support.css +62 -0
  9. trunk/assets/images/connect.svg +86 -0
  10. trunk/assets/images/loading.gif +0 -0
  11. trunk/assets/images/printful-logo-footer.png +0 -0
  12. trunk/assets/images/printful-menu-icon.png +0 -0
  13. trunk/assets/js/block-loader.js +22 -0
  14. trunk/assets/js/connect.js +60 -0
  15. trunk/assets/js/intercom.min.js +12 -0
  16. trunk/assets/js/product-customizer.js +207 -0
  17. trunk/assets/js/product-size-guide.js +468 -0
  18. trunk/assets/js/settings.js +44 -0
  19. trunk/i18n/languages/printful.pot +695 -0
  20. trunk/includes/class-printful-admin-dashboard.php +270 -0
  21. trunk/includes/class-printful-admin-settings.php +466 -0
  22. trunk/includes/class-printful-admin-status.php +597 -0
  23. trunk/includes/class-printful-admin-support.php +238 -0
  24. trunk/includes/class-printful-admin.php +172 -0
  25. trunk/includes/class-printful-carriers.php +90 -0
  26. trunk/includes/class-printful-client.php +192 -0
  27. trunk/includes/class-printful-customizer.php +168 -0
  28. trunk/includes/class-printful-integration.php +165 -0
  29. trunk/includes/class-printful-request-log.php +151 -0
  30. trunk/includes/class-printful-rest-api-controller.php +263 -0
  31. trunk/includes/class-printful-shipping.php +337 -0
  32. trunk/includes/class-printful-size-chart-tab.php +131 -0
  33. trunk/includes/class-printful-size-guide.php +221 -0
  34. trunk/includes/class-printful-taxes.php +189 -0
  35. trunk/includes/class-printful-template.php +77 -0
  36. trunk/includes/templates/ajax-loader.php +11 -0
  37. trunk/includes/templates/connect.php +40 -0
  38. trunk/includes/templates/customizer-hidden-input.php +1 -0
  39. trunk/includes/templates/error.php +1 -0
  40. trunk/includes/templates/footer.php +1 -0
  41. trunk/includes/templates/header.php +20 -0
  42. trunk/includes/templates/inline-script.php +3 -0
  43. trunk/includes/templates/order-table.php +69 -0
  44. trunk/includes/templates/personalize-button.php +12 -0
  45. trunk/includes/templates/quick-links.php +32 -0
  46. trunk/includes/templates/setting-group.php +105 -0
  47. trunk/includes/templates/setting-submit.php +24 -0
  48. trunk/includes/templates/shipping-notification.php +4 -0
  49. trunk/includes/templates/size-guide-button.php +10 -0
  50. trunk/includes/templates/stats.php +44 -0
  51. trunk/includes/templates/status-report.php +23 -0
  52. trunk/includes/templates/status-table.php +56 -0
  53. trunk/includes/templates/support-info.php +27 -0
  54. trunk/license.txt +697 -0
  55. trunk/printful-shipping.php +142 -0
  56. trunk/readme.txt +438 -0
printful-shipping.php CHANGED
@@ -20,7 +20,7 @@ if ( ! defined( 'PF_PLUGIN_FILE' ) ) {
20
 
21
  class Printful_Base {
22
 
23
- const VERSION = '2.1.31';
24
  const PF_HOST = 'https://www.printful.com/';
25
  const PF_API_HOST = 'https://api.printful.com/';
26
 
20
 
21
  class Printful_Base {
22
 
23
+ const VERSION = '2.1.32';
24
  const PF_HOST = 'https://www.printful.com/';
25
  const PF_API_HOST = 'https://api.printful.com/';
26
 
readme.txt CHANGED
@@ -2,9 +2,9 @@
2
  Contributors: girts_u, kievins, kberzins
3
  Tags: woocommerce, printful, drop shipping, shipping, shipping rates, fulfillment, printing, fedex, carriers, checkout, t-shirts
4
  Requires at least: 5.3
5
- Tested up to: 5.9
6
  Requires PHP: 5.6
7
- Stable tag: 2.1.31
8
  License: GPLv3 or later
9
  License URI: https://www.gnu.org/licenses/gpl-3.0.en.html
10
 
@@ -65,6 +65,10 @@ Go to https://www.printful.com/dashboard/store , select your WooCommerce store,
65
 
66
  == Upgrade Notice ==
67
 
 
 
 
 
68
  = 2.1.31 =
69
  Licence updated to GPLv3. WordPress compatibility raised to 5.9 and Woocommerce compatibility raised to 6.4
70
 
@@ -235,6 +239,10 @@ First release
235
 
236
  == Changelog ==
237
 
 
 
 
 
238
  = 2.1.31 =
239
  * Licence updated to GPLv3
240
  * WordPress compatibility raised to 5.9
2
  Contributors: girts_u, kievins, kberzins
3
  Tags: woocommerce, printful, drop shipping, shipping, shipping rates, fulfillment, printing, fedex, carriers, checkout, t-shirts
4
  Requires at least: 5.3
5
+ Tested up to: 6.0
6
  Requires PHP: 5.6
7
+ Stable tag: 2.1.32
8
  License: GPLv3 or later
9
  License URI: https://www.gnu.org/licenses/gpl-3.0.en.html
10
 
65
 
66
  == Upgrade Notice ==
67
 
68
+ = 2.1.32 =
69
+ Woocommerce compatibility raised to 6.5.1
70
+ WordPress compatibility raised to 6.0
71
+
72
  = 2.1.31 =
73
  Licence updated to GPLv3. WordPress compatibility raised to 5.9 and Woocommerce compatibility raised to 6.4
74
 
239
 
240
  == Changelog ==
241
 
242
+ = 2.1.32 =
243
+ Woocommerce compatibility raised to 6.5.1
244
+ WordPress compatibility raised to 6.0
245
+
246
  = 2.1.31 =
247
  * Licence updated to GPLv3
248
  * WordPress compatibility raised to 5.9
trunk/assets/css/dashboard.css ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .printful-connect {
2
+ position: relative;
3
+ height: 100%;
4
+ }
5
+
6
+ .printful-connect-inner {
7
+ max-width: 600px;
8
+ margin: 100px auto 0 auto;
9
+ text-align: center;
10
+ position: relative;
11
+ }
12
+ .printful-connect .connect-image {
13
+ max-width: 100%;
14
+ }
15
+ .printful-connect h1 {
16
+ margin-bottom: 30px;
17
+ font-size: 34px;
18
+ }
19
+ .printful-connect .connect-description {
20
+ margin: 20px 0 30px 0;
21
+ }
22
+
23
+ /*noinspection CssUnusedSymbol*/
24
+ .printful-connect .printful-connect-button {
25
+ height: 53px;
26
+ line-height: 53px;
27
+ padding: 0 40px;
28
+ font-size: 18px;
29
+ }
30
+
31
+ .printful-stats {
32
+ position: relative;
33
+ margin: 20px 0 50px 0;
34
+ background-color: #fff;
35
+ padding: 30px 20px;
36
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
37
+ border-left: #0073aa 4px solid;
38
+ }
39
+ .printful-stats .printful-stats-item {
40
+ width: 49%;
41
+ display: inline-block;
42
+ margin-bottom: 25px;
43
+ box-sizing: border-box;
44
+ vertical-align: top;
45
+ padding: 0 8px;
46
+ }
47
+
48
+ .printful-stats .printful-stats-item h4 {
49
+ margin: 0 0 10px 0;
50
+ font-size: 24px;
51
+ line-height: 26px;
52
+ }
53
+ .printful-stats .printful-stats-item h4 .dashicons {
54
+ font-size: 26px;
55
+ width: 26px;
56
+ height: 26px;
57
+ }
58
+
59
+ /*noinspection ALL*/
60
+ .printful-stats .printful-stats-item h4 .dashicons-arrow-up-alt {
61
+ color: green;
62
+ }
63
+ .printful-stats .printful-stats-item h4 .dashicons-arrow-down-alt {
64
+ color: #d54e21;
65
+ }
66
+
67
+ @media (min-width: 768px) {
68
+ .printful-stats .printful-stats-item {
69
+ width: 24%;
70
+ margin-bottom: 0;
71
+ padding: 0 25px;
72
+ }
73
+ .printful-stats .printful-stats-item h4 {
74
+ font-size: 32px;
75
+ line-height: 36px;
76
+ }
77
+ .printful-stats .printful-stats-item h4 .dashicons {
78
+ padding-top: 4px;
79
+ font-size: 30px;
80
+ width: 30px;
81
+ height: 30px;
82
+ }
83
+ }
84
+
85
+ .printful-latest-orders {
86
+ display: table;
87
+ width: 100%;
88
+ margin-bottom: 50px;
89
+ }
90
+ .printful-latest-orders .row {
91
+ display: table-row;
92
+ }
93
+ .printful-latest-orders .row .cell {
94
+ display: table-cell;
95
+ }
96
+
97
+ .wrap h2.nav-tab-wrapper.printful-tabs {
98
+ margin-bottom: 15px;
99
+ }
100
+
101
+
102
+ .printful-quick-links {
103
+ position: relative;
104
+ margin-bottom: 30px;
105
+ }
106
+ .printful-quick-links .printful-quick-links-item {
107
+ width: 120px;
108
+ background: #fff;
109
+ border: solid 1px #e1e1e1;
110
+ float: left;
111
+ padding: 20px;
112
+ margin-right: 25px;
113
+ margin-bottom: 25px;
114
+ text-align: center;
115
+ text-decoration: none;
116
+ }
117
+ .printful-quick-links .printful-quick-links-item .dashicons {
118
+ font-size: 40px;
119
+ width: 40px;
120
+ height: 40px;
121
+ text-decoration: none;
122
+ color: #444;
123
+ }
124
+ .printful-quick-links .printful-quick-links-item h4 {
125
+ text-decoration: none;
126
+ color: #444;
127
+ margin: 5px 0 0 0;
128
+ }
129
+ .printful-quick-links .printful-quick-links-item:hover {
130
+ cursor: pointer;
131
+ }
132
+ .printful-quick-links .printful-quick-links-item:hover .dashicons,
133
+ .printful-quick-links .printful-quick-links-item:hover h4 {
134
+ color: #0D72B2;
135
+ }
136
+
137
+ .printful-error {
138
+ margin-top: 20px;
139
+ }
140
+
141
+ .printful-notice {
142
+ background: #fff;
143
+ border-left: 4px solid #dc3232;
144
+ padding: 1px 12px;
145
+ margin: 10px 0 20px 0;
146
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
147
+ text-align: left;
148
+ }
149
+ .printful-notice li {
150
+ font-weight: bold;
151
+ margin-left: 20px;
152
+ list-style: disc;
153
+ }
154
+
155
+ .block-loader {
156
+ text-align: center;
157
+ padding: 20px 0;
158
+ line-height: 20px;
159
+ }
160
+ .block-loader .message {
161
+ padding-left: 10px;
162
+ line-height: 20px;
163
+ display: inline-block;
164
+ font-size: 16px;
165
+ }
166
+ .block-loader .loader {
167
+ vertical-align: top;
168
+ }
169
+
170
+ #wpfooter::after {
171
+ box-sizing: border-box;
172
+ content: "";
173
+ width: 64px;
174
+ left: 50%;
175
+ margin-left: -32px;
176
+ height: 28px;
177
+ position: absolute;
178
+ bottom: 0;
179
+ background: url("../images/printful-logo-footer.png") no-repeat bottom center;
180
+ background-size: 64px;
181
+ }
trunk/assets/css/global.css ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .printful-toolbar .printful-toolbar-issues {
2
+ background-color: #d54e21;
3
+ display: inline-block;
4
+ padding: 0 5px !important;
5
+ border-radius: 10px !important;
6
+ line-height: 16px !important;
7
+ font-size: 11px !important;
8
+ color: #fff !important;
9
+ }
10
+ .printful-toolbar:hover .printful-toolbar-issues,
11
+ .printful-toolbar:active .printful-toolbar-issues,
12
+ .printful-toolbar.hover .printful-toolbar-issues
13
+ {
14
+ color: #fff !important;
15
+ }
16
+
17
+ #adminmenu .toplevel_page_printful-dashboard img {
18
+ width: 28px;
19
+ }
trunk/assets/css/settings.css ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .printful-setting-group {
2
+ overflow: hidden;
3
+ margin-bottom: 20px;
4
+ }
5
+
6
+ .printful-setting-group .input-text {
7
+ width: 400px;
8
+ }
9
+
10
+ .printful-setting-group .checkbox-group-item i {
11
+ color: #777;
12
+ }
13
+
14
+ .printful-submit .woocommerce-save-button {
15
+ float: left;
16
+ }
17
+
18
+ .printful-submit .loader-wrap {
19
+ display: inline-block;
20
+ padding: 4px 10px;
21
+ float: left;
22
+ }
23
+
24
+ .printful-submit .loader-wrap .loader {
25
+ display: none;
26
+ float: left;
27
+ }
28
+
29
+ .printful-submit .loader-wrap .dashicons {
30
+ font-size: 24px;
31
+ width: 24px;
32
+ height: 24px;
33
+ }
34
+
35
+ .printful-submit .loader-wrap .pass,
36
+ .printful-submit .loader-wrap .fail {
37
+ display: none;
38
+ float: left;
39
+ line-height: 24px;
40
+ }
41
+
42
+ .printful-setting-group .carrier-type {
43
+ font-weight: bold;
44
+ padding: 20px 0;
45
+ display: block;
46
+ }
trunk/assets/css/size-guide.css ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .pf-size-guide-modal-wrapper {
2
+ position: fixed;
3
+ z-index: 2147483648;
4
+ padding: 20px;
5
+ top: 0;
6
+ width: 100%;
7
+ height: 100%;
8
+ left: 0;
9
+ background: rgba(0, 0, 0, 0.6);
10
+ box-sizing: border-box;
11
+ }
12
+
13
+ .pf-size-guide-modal {
14
+ background-color: #fff;
15
+ width: 100%;
16
+ height: 100%;
17
+ max-width: 900px;
18
+ overflow: hidden;
19
+ overflow-y: auto;
20
+ position: relative;
21
+ margin: auto;
22
+ }
23
+
24
+ .pf-size-guide-modal__close {
25
+ height: 30px;
26
+ width: 30px;
27
+ cursor: pointer;
28
+ border: 0;
29
+ background: 0 0;
30
+ padding: 0;
31
+ -webkit-appearance: none;
32
+ float: right;
33
+ background: none !important;
34
+ }
35
+
36
+ .pf-size-guide-modal__header {
37
+ width: 100%;
38
+ box-sizing: border-box;
39
+ padding: 15px;
40
+ }
41
+
42
+ .pf-size-guide-modal__title {
43
+ float: left;
44
+ font-weight: bold;
45
+ margin: 0;
46
+ clear: none;
47
+ }
48
+
49
+ .pf-size-guide-modal__close img {
50
+ height: 30px;
51
+ width: 30px;
52
+ }
53
+
54
+ .pf-size-guide-modal__content {
55
+ padding: 15px;
56
+ }
57
+
58
+ .pf-size-guide-modal__content h4 {
59
+ margin: 24px 0 16px;
60
+ }
61
+ .pf-size-guide-modal__content p {
62
+ margin: 0 0 10px;
63
+ }
64
+ .pf-size-guide-modal-size-chart {
65
+ overflow-x: auto;
66
+ }
67
+
68
+ .pf-product-size-guide__tabs {
69
+ white-space: nowrap;
70
+ }
71
+
72
+ .pf-product-size-guide__tabs,
73
+ .pf-size-guide-modal-size-chart__tabs {
74
+ list-style: none;
75
+ padding: 0;
76
+ margin: 0;
77
+ box-shadow: inset 0 -1px 0 0 #E5E5E5;
78
+ }
79
+
80
+ .pf-product-size-guide__tab {
81
+ cursor: pointer;
82
+ vertical-align: bottom;
83
+ margin-right: 8px;
84
+ display: inline-block;
85
+ padding: 12px 16px;
86
+ border: 1px solid #F8F8F8;
87
+ border-bottom: 1px solid #E5E5E5;
88
+ }
89
+
90
+ .pf-product-size-guide__tab.pf-product-size-guide__tab--active {
91
+ border: solid 1px #E5E5E5;
92
+ border-bottom: none;
93
+ }
94
+
95
+ .pf-size-guide-modal-size-chart__tab {
96
+ font-size: 15px;
97
+ font-weight: bold;
98
+ border: solid 1px transparent;
99
+ cursor: pointer;
100
+ vertical-align: bottom;
101
+ margin-right: 8px;
102
+ display: inline-block;
103
+ padding: 12px 8px;
104
+ text-transform: capitalize;
105
+ }
106
+ .pf-size-guide-modal-size-chart__tab.pf-size-guide-modal-size-chart__tab--active {
107
+ box-shadow: inset 0 -3px #222;
108
+ font-weight: bold;
109
+ }
110
+
111
+ .pf-size-guide-modal-measurements {
112
+ padding: 15px 0;
113
+ }
114
+
115
+ .pf-size-guide-modal-measurements__description {
116
+ float: left;
117
+ width: 65%;
118
+ position: relative;
119
+ }
120
+
121
+ .pf-size-guide-modal-measurements__image {
122
+ position: relative;
123
+ width: 35%;
124
+ padding: 5px;
125
+ text-align: center;
126
+ float: left;
127
+ }
128
+
129
+ @media only screen and (max-width: 960px) {
130
+ .pf-size-guide-modal-measurements__image,
131
+ .pf-size-guide-modal-measurements__description {
132
+ float: none;
133
+ display: block;
134
+ width: 100%;
135
+ }
136
+
137
+ .pf-product-size-guide__tabs {
138
+ overflow-x: scroll;
139
+ }
140
+ }
141
+
142
+ .pf-size-guide-modal-size-chart__table {
143
+ border-collapse: collapse;
144
+ width: 100%;
145
+ overflow-x: auto;
146
+ }
147
+
148
+ .pf-size-guide-modal-size-chart__table thead {
149
+ text-transform: uppercase;
150
+ font-weight: bold;
151
+ }
152
+
153
+ .pf-size-guide-modal-size-chart__table tr {
154
+ border: none;
155
+ border-collapse: collapse;
156
+ }
157
+
158
+ .pf-size-guide-modal-size-chart__table td {
159
+ border: none;
160
+ border-bottom: 1px solid #E5E5E5;
161
+ font-size: 16px;
162
+ text-transform: uppercase;
163
+ padding: 12px 8px;
164
+ }
165
+
166
+ .pf-size-guide-modal-clear {
167
+ clear: both;
168
+ }
169
+
trunk/assets/css/status.css ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .pass {
2
+ font-weight: bold;
3
+ color: green;
4
+ }
5
+ .fail {
6
+ font-weight: bold;
7
+ color: red;
8
+ }
9
+ .warning {
10
+ font-weight: bold;
11
+ color: #cab827;
12
+ }
13
+ .printful-status .col-status {
14
+ width: 10%;
15
+ }
16
+ .printful-status .col-desc {
17
+ width: 57%;
18
+ }
19
+ .printful-status tbody tr:first-child td {
20
+ font-weight: bold;
21
+ }
trunk/assets/css/support.css ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .support-report-wrap {
2
+ background-color: #fff;
3
+ padding: 20px;
4
+ margin: 20px 0;
5
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
6
+ border-left: #0073aa 4px solid;
7
+ }
8
+
9
+ .support-report-wrap p {
10
+ margin-top: 0;
11
+ }
12
+
13
+ .support-report-wrap .support-report {
14
+ width: 100%;
15
+ height: 100px;
16
+ font-family: monospace;
17
+ margin-bottom: 10px;
18
+ background: #efefef;
19
+ }
20
+
21
+ .support-info-wrap {
22
+ margin-top: 40px;
23
+ box-sizing: border-box;
24
+
25
+ }
26
+
27
+ .support-info-wrap .support-info-block {
28
+ box-sizing: border-box;
29
+ padding: 20px;
30
+ background: #fff;
31
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,.1);
32
+ margin-bottom: 20px;
33
+ }
34
+
35
+ .support-info-wrap .support-info-block h3 {
36
+ text-align: center;
37
+ margin-top: 10px;
38
+ }
39
+
40
+ .support-info-wrap .support-info-block .btn-wrap {
41
+ margin-top: 20px;
42
+ text-align: center;
43
+ }
44
+
45
+ @media (min-width: 768px) {
46
+
47
+ .support-info-wrap {
48
+ justify-content: space-between;
49
+ display: flex;
50
+ }
51
+
52
+ .support-info-wrap .support-info-block {
53
+ width: 32%;
54
+ }
55
+
56
+ .support-info-wrap .support-info-block:first-child {
57
+
58
+ }
59
+ .support-info-wrap .support-info-block:last-child {
60
+
61
+ }
62
+ }
trunk/assets/images/connect.svg ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1544 500">
2
+ <defs>
3
+ <style>
4
+ .cls-1{isolation:isolate;}.cls-14,.cls-17,.cls-19,.cls-2,.cls-20,.cls-21,.cls-22,.cls-4{fill:none;}.cls-2{stroke:#94e0de;}.cls-14,.cls-17,.cls-2,.cls-3,.cls-4,.cls-5{stroke-miterlimit:10;stroke-width:2.45px;}.cls-3,.cls-7{fill:#fff;}.cls-3,.cls-4{stroke:#14aba3;}.cls-18,.cls-5{fill:#d4f2f2;}.cls-5{stroke:#f2fcfa;opacity:0.5;}.cls-6{fill:#7f54b3;}.cls-8{fill:#f2c994;}.cls-9{fill:#ed4642;}.cls-10{fill:#17bcb5;}.cls-11{fill:#df392f;}.cls-12{fill:#16342f;}.cls-13{fill:#15291a;}.cls-14{stroke:#7ccdcd;}.cls-15{fill:#b5e8e8;}.cls-16{fill:#7ccdcd;}.cls-17{stroke:#f2c994;mix-blend-mode:multiply;}.cls-19,.cls-20{stroke:#d4f2f2;}.cls-19,.cls-20,.cls-21,.cls-22{stroke-linecap:round;stroke-linejoin:round;stroke-width:8px;}.cls-20{stroke-dasharray:0.11
5
+ 32.4;}.cls-21,.cls-22{stroke:#fff;}.cls-22{stroke-dasharray:0.12 35.99;}
6
+ </style>
7
+ </defs>
8
+ <g class="cls-1">
9
+ <g id="Layer_1" data-name="Layer 1">
10
+ <line class="cls-2" x1="70.8" y1="406.89" x2="1473.2" y2="406.89"/>
11
+ <polyline class="cls-3"
12
+ points="450.35 369.86 438.55 416.13 435.15 416.13 430.24 427.63 557.18 427.63 553.19 416.13 548.87 416.13 537.07 369.86 728.32 369.86 728.32 87.29 259.11 87.29 259.11 369.86 517.61 369.86"/>
13
+ <circle class="cls-4" cx="493.71" cy="349.28" r="7.77"/>
14
+ <rect class="cls-5" x="378.65" y="-6.51" width="231.45" height="443.06"
15
+ transform="translate(709.39 -279.35) rotate(90)"/>
16
+ <path class="cls-6"
17
+ d="M395.41,154.11h181a20.71,20.71,0,0,1,20.72,20.73v69.08a20.71,20.71,0,0,1-20.72,20.72h-64.9l8.91,21.81-39.18-21.81H395.5a20.71,20.71,0,0,1-20.72-20.72V174.84a20.64,20.64,0,0,1,20.63-20.73Z"/>
18
+ <path class="cls-7"
19
+ d="M387.44,173a7.25,7.25,0,0,1,5.69-2.8q6.92-.54,7.86,6.51c2.8,18.89,5.88,34.88,9.13,48L429.91,187q2.71-5.14,6.78-5.42c4-.27,6.42,2.26,7.41,7.59a168.51,168.51,0,0,0,8.59,30.91q3.53-34.44,11.93-49.71a6.93,6.93,0,0,1,6-4,7.9,7.9,0,0,1,5.7,1.81,7.1,7.1,0,0,1,2.8,5.15,7.8,7.8,0,0,1-.91,4.34c-3.52,6.51-6.41,17.44-8.76,32.63-2.26,14.73-3.07,26.21-2.53,34.43a10.65,10.65,0,0,1-1.09,6A5.82,5.82,0,0,1,461,254c-2.35.18-4.79-.91-7.14-3.35-8.4-8.58-15.09-21.41-20-38.5q-8.81,17.35-13,26c-5.34,10.21-9.85,15.46-13.65,15.73-2.44.18-4.52-1.9-6.33-6.24Q394,229.89,386,179.06a7.7,7.7,0,0,1,1.45-6.06Zm194.22,14.19a20.79,20.79,0,0,0-14.46-10.48,23.24,23.24,0,0,0-4.88-.55c-8.68,0-15.73,4.52-21.24,13.56A47.89,47.89,0,0,0,534,215.21q0,10.44,4.34,17.89a20.79,20.79,0,0,0,14.46,10.48,23.17,23.17,0,0,0,4.88.55c8.77,0,15.81-4.52,21.24-13.56A48.43,48.43,0,0,0,586,205,33.36,33.36,0,0,0,581.66,187.19Zm-11.39,25c-1.26,6-3.52,10.4-6.87,13.38-2.62,2.35-5.06,3.34-7.32,2.89s-4-2.35-5.33-5.87a23.07,23.07,0,0,1-1.63-8.23,30.77,30.77,0,0,1,.64-6.59A29.2,29.2,0,0,1,554.55,197c3-4.43,6.14-6.23,9.4-5.6,2.16.45,4,2.35,5.33,5.87a23,23,0,0,1,1.62,8.23,31.71,31.71,0,0,1-.63,6.68Zm-45.19-25a21,21,0,0,0-14.46-10.48,23.1,23.1,0,0,0-4.88-.55c-8.67,0-15.72,4.52-21.24,13.56a47.89,47.89,0,0,0-7.05,25.49q0,10.44,4.34,17.89a20.83,20.83,0,0,0,14.46,10.48,23.24,23.24,0,0,0,4.88.55c8.77,0,15.82-4.52,21.24-13.56a48.43,48.43,0,0,0,7-25.58C529.42,197.94,528,192.07,525.08,187.19Zm-11.47,25c-1.27,6-3.53,10.4-6.87,13.38-2.62,2.35-5.06,3.34-7.32,2.89s-4-2.35-5.34-5.87a23,23,0,0,1-1.62-8.23,31.42,31.42,0,0,1,.63-6.59A29.2,29.2,0,0,1,497.88,197c3-4.43,6.15-6.23,9.4-5.6,2.17.45,4,2.35,5.33,5.87a22.85,22.85,0,0,1,1.63,8.23,26.7,26.7,0,0,1-.63,6.68Z"/>
20
+ <polyline class="cls-3"
21
+ points="1082.61 369.86 1070.82 416.13 1067.42 416.13 1062.51 427.63 1189.45 427.63 1185.46 416.13 1181.14 416.13 1169.34 369.86 1360.58 369.86 1360.58 87.29 891.37 87.29 891.37 369.86 1148.84 369.86"/>
22
+ <circle class="cls-4" cx="1125.98" cy="349.28" r="7.77"/>
23
+ <rect class="cls-5" x="1010.91" y="-6.51" width="231.45" height="443.06"
24
+ transform="translate(1341.66 -911.62) rotate(90)"/>
25
+ <g id="pf-logo-color-white-1-svg">
26
+ <polygon class="cls-8"
27
+ points="1067.23 166.57 1010.78 263.47 1061.75 263.47 1092.72 210.32 1067.23 166.57"/>
28
+ <polygon class="cls-9"
29
+ points="1143.69 210.32 1118.21 166.57 1092.72 210.32 1118.21 254.07 1143.69 210.32"/>
30
+ <polygon class="cls-10"
31
+ points="1169.18 166.57 1143.69 210.32 1174.66 263.47 1225.64 263.47 1169.18 166.57"/>
32
+ <polygon class="cls-11" points="1092.72 210.32 1061.75 263.47 1092.72 210.32 1092.72 210.32"/>
33
+ <polygon class="cls-11"
34
+ points="1118.21 254.07 1118.21 254.07 1092.72 210.32 1092.72 210.32 1118.21 254.07"/>
35
+ <polygon class="cls-11"
36
+ points="1092.72 210.32 1061.75 263.47 1112.73 263.47 1118.21 254.07 1092.72 210.32"/>
37
+ <polygon class="cls-12"
38
+ points="1118.21 254.07 1143.69 210.32 1143.69 210.32 1118.21 254.07 1118.21 254.07"/>
39
+ <polygon class="cls-12" points="1143.69 210.32 1174.66 263.47 1143.69 210.32 1143.69 210.32"/>
40
+ <polygon class="cls-12"
41
+ points="1118.21 254.07 1118.21 254.07 1118.21 254.07 1123.68 263.47 1174.66 263.47 1143.69 210.32 1118.21 254.07"/>
42
+ <polygon class="cls-12" points="1118.21 254.07 1112.73 263.47 1118.21 254.07 1118.21 254.07"/>
43
+ <polygon class="cls-12" points="1118.21 254.07 1112.73 263.47 1118.21 254.07 1118.21 254.07"/>
44
+ <polygon class="cls-13" points="1118.21 254.07 1123.68 263.47 1118.21 254.07 1118.21 254.07"/>
45
+ <polygon class="cls-13" points="1118.21 254.07 1123.68 263.47 1118.21 254.07 1118.21 254.07"/>
46
+ <polygon class="cls-13" points="1112.73 263.47 1123.68 263.47 1118.21 254.07 1112.73 263.47"/>
47
+ </g>
48
+ <line class="cls-14" x1="189.21" y1="318.47" x2="193.29" y2="383.94"/>
49
+ <line class="cls-14" x1="162.35" y1="333.34" x2="192.05" y2="364.06"/>
50
+ <line class="cls-14" x1="192.36" y1="369.04" x2="224.16" y2="299.56"/>
51
+ <line class="cls-14" x1="191.95" y1="371.29" x2="218.51" y2="351.84"/>
52
+ <ellipse class="cls-15" cx="240.1" cy="317.96" rx="20.13" ry="29.72"
53
+ transform="translate(-147.91 227.44) rotate(-39.81)"/>
54
+ <path class="cls-15"
55
+ d="M167.46,331.05c11.61,17.08-12.63,34.1-27.24,36.66-8.56,1.49-19.26-.41-19.72-10.8-.53-11.76,9.59-24.08,19.41-29.46a24.63,24.63,0,0,1,4.65-2C152.39,323,162.45,323.68,167.46,331.05Z"/>
56
+ <path class="cls-15"
57
+ d="M149,294.37a46.36,46.36,0,0,0,15.7,23c8.81,6.81,22.1,9.14,31.48,2.19,5.67-4.2,9.37-10.67,11.23-17.48,5.31-19.48-5.46-42.7-26.8-46-9.89-1.54-20.59,2.24-26.88,10C147,274.47,146.16,284.77,149,294.37Z"/>
58
+ <circle class="cls-16" cx="158.58" cy="336.64" r="2.77"/>
59
+ <circle class="cls-16" cx="229.25" cy="303.8" r="2.52"/>
60
+ <circle class="cls-16" cx="187.19" cy="307.57" r="3.07"/>
61
+ <polygon class="cls-8" points="229.69 442.79 163.22 442.79 153.72 386.53 239.18 386.53 229.69 442.79"/>
62
+ <polygon class="cls-8" points="242.92 388.08 149.99 388.08 146.27 371.63 246.64 371.63 242.92 388.08"/>
63
+ <line class="cls-17" x1="238.92" y1="389.01" x2="164.54" y2="389.01"/>
64
+ <path class="cls-15"
65
+ d="M238.12,356.33a19.56,19.56,0,0,1,1.64,16.34c-4.87,13.61-22.21,8.88-29.66.49a20.07,20.07,0,0,1-5.24-13,13.85,13.85,0,0,1,9.62-13.89C224,343,233.56,348.28,238.12,356.33Z"/>
66
+ <circle class="cls-16" cx="218.25" cy="357.26" r="1.66"/>
67
+ <circle class="cls-10" cx="801.57" cy="233.66" r="110.07" transform="translate(69.55 635.23) rotate(-45)"/>
68
+ <path class="cls-7"
69
+ d="M801.57,125.87A107.79,107.79,0,1,1,693.78,233.66,107.79,107.79,0,0,1,801.57,125.87m0-4.56A112.34,112.34,0,1,0,881,154.22a111.61,111.61,0,0,0-79.45-32.91Z"/>
70
+ <polygon class="cls-18"
71
+ points="839.16 181.27 826.43 205.35 839.16 229.44 823 229.44 810.26 205.35 823 181.27 839.16 181.27"/>
72
+ <line class="cls-19" x1="788.83" y1="205.35" x2="788.77" y2="205.35"/>
73
+ <line class="cls-20" x1="756.37" y1="205.35" x2="740.06" y2="205.35"/>
74
+ <line class="cls-19" x1="723.87" y1="205.35" x2="723.81" y2="205.35"/>
75
+ <line class="cls-21" x1="723.67" y1="262.05" x2="723.73" y2="262.05"/>
76
+ <line class="cls-21" x1="750.61" y1="262.05" x2="750.67" y2="262.05"/>
77
+ <line class="cls-19" x1="852.47" y1="205.35" x2="852.53" y2="205.35"/>
78
+ <line class="cls-19" x1="879.41" y1="205.35" x2="879.47" y2="205.35"/>
79
+ <polygon class="cls-7"
80
+ points="758.66 286.05 771.39 261.97 758.66 237.88 774.83 237.88 787.56 261.97 774.83 286.05 758.66 286.05"/>
81
+ <line class="cls-21" x1="876.2" y1="262.72" x2="876.15" y2="262.72"/>
82
+ <line class="cls-22" x1="840.15" y1="262.72" x2="822.03" y2="262.72"/>
83
+ <line class="cls-21" x1="804.04" y1="262.72" x2="803.98" y2="262.72"/>
84
+ </g>
85
+ </g>
86
+ </svg>
trunk/assets/images/loading.gif ADDED
Binary file
trunk/assets/images/printful-logo-footer.png ADDED
Binary file
trunk/assets/images/printful-menu-icon.png ADDED
Binary file
trunk/assets/js/block-loader.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Printful_Block_Loader;
2
+
3
+ (function () {
4
+ 'use strict';
5
+
6
+ Printful_Block_Loader = {
7
+ load: function (ajax_url, block) {
8
+
9
+ block = jQuery('#' + block);
10
+ if (block.length > 0) {
11
+
12
+ jQuery.ajax({
13
+ type: "GET",
14
+ url: ajax_url,
15
+ success: function (response) {
16
+ block.html(response);
17
+ }
18
+ });
19
+ }
20
+ }
21
+ };
22
+ })();
trunk/assets/js/connect.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Printful_Connect;
2
+
3
+ (function () {
4
+ 'use strict';
5
+
6
+ Printful_Connect = {
7
+ interval: 0,
8
+ ajax_url: '',
9
+ init: function (ajax_url) {
10
+ this.ajax_url = ajax_url;
11
+ this.loader();
12
+ this.listen_status();
13
+ this.listen_auth_return();
14
+ },
15
+ loader: function () {
16
+ jQuery('.printful-connect-button').click(function () {
17
+ jQuery(this).hide();
18
+ jQuery(this).siblings('.loader').removeClass('hidden');
19
+
20
+ setTimeout(function() {
21
+ Printful_Connect.hide_loader();
22
+ }, 60000); //hide the loader after a minute, assume failure
23
+ });
24
+ },
25
+ hide_loader: function() {
26
+ var button = jQuery('.printful-connect-button');
27
+ button.show();
28
+ button.siblings('.loader').addClass('hidden');
29
+ },
30
+ listen_status: function () {
31
+ this.interval = setInterval(this.get_status.bind(this), 10000); //check status every 10 secs
32
+ },
33
+ get_status: function () {
34
+ var interval = this.interval;
35
+ jQuery.ajax( {
36
+ type: "GET",
37
+ url: this.ajax_url,
38
+ success: function( response ) {
39
+ if (response === 'OK') {
40
+ clearInterval(interval);
41
+ Printful_Connect.send_return_message();
42
+ }
43
+ }
44
+ });
45
+ },
46
+ listen_auth_return: function () {
47
+ var intercom = Intercom.getInstance();
48
+ intercom.on('printful-auth', function (data) {
49
+ if (data.success === true) {
50
+ location.reload();
51
+ }
52
+ });
53
+ },
54
+ send_return_message: function () {
55
+ var intercom = Intercom.getInstance();
56
+ intercom.emit('printful-auth', {success: true});
57
+ window.top.close();
58
+ }
59
+ };
60
+ })();
trunk/assets/js/intercom.min.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*! intercom.js | https://github.com/diy/intercom.js | Apache License (v2) */
2
+ var Intercom=function(){var g=function(){};g.createInterface=function(b){return{on:function(a,c){"undefined"===typeof this[b]&&(this[b]={});this[b].hasOwnProperty(a)||(this[b][a]=[]);this[b][a].push(c)},off:function(a,c){"undefined"!==typeof this[b]&&this[b].hasOwnProperty(a)&&i.removeItem(c,this[b][a])},trigger:function(a){if("undefined"!==typeof this[b]&&this[b].hasOwnProperty(a))for(var c=Array.prototype.slice.call(arguments,1),e=0;e<this[b][a].length;e++)this[b][a][e].apply(this[b][a][e],c)}}};
3
+ var m=g.createInterface("_handlers");g.prototype._on=m.on;g.prototype._off=m.off;g.prototype._trigger=m.trigger;var n=g.createInterface("handlers");g.prototype.on=function(){n.on.apply(this,arguments);Array.prototype.unshift.call(arguments,"on");this._trigger.apply(this,arguments)};g.prototype.off=n.off;g.prototype.trigger=n.trigger;var f=window.localStorage;"undefined"===typeof f&&(f={getItem:function(){},setItem:function(){},removeItem:function(){}});var i={},h=function(){return(65536*(1+Math.random())|
4
+ 0).toString(16).substring(1)};i.guid=function(){return h()+h()+"-"+h()+"-"+h()+"-"+h()+"-"+h()+h()+h()};i.throttle=function(b,a){var c=0;return function(){var e=(new Date).getTime();e-c>b&&(c=e,a.apply(this,arguments))}};i.extend=function(b,a){if("undefined"===typeof b||!b)b={};if("object"===typeof a)for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b};i.removeItem=function(b,a){for(var c=a.length-1;0<=c;c--)a[c]===b&&a.splice(c,1);return a};var d=function(){var b=this,a=(new Date).getTime();
5
+ this.origin=i.guid();this.lastMessage=a;this.bindings=[];this.receivedIDs={};this.previousValues={};a=function(){b._onStorageEvent.apply(b,arguments)};window.attachEvent?document.attachEvent("onstorage",a):window.addEventListener("storage",a,!1)};d.prototype._transaction=function(b){var a=this,c=!1,e=!1,p=null,d=function(){if(!c){var g=(new Date).getTime(),s=parseInt(f.getItem(l)||0);s&&1E3>g-s?(e||(a._on("storage",d),e=!0),p=window.setTimeout(d,20)):(c=!0,f.setItem(l,g),b(),e&&a._off("storage",d),
6
+ p&&window.clearTimeout(p),f.removeItem(l))}};d()};d.prototype._cleanup_emit=i.throttle(100,function(){this._transaction(function(){for(var b=(new Date).getTime()-t,a=0,c=JSON.parse(f.getItem(j)||"[]"),e=c.length-1;0<=e;e--)c[e].timestamp<b&&(c.splice(e,1),a++);0<a&&f.setItem(j,JSON.stringify(c))})});d.prototype._cleanup_once=i.throttle(100,function(){var b=this;this._transaction(function(){var a,c=JSON.parse(f.getItem(k)||"{}");(new Date).getTime();var e=0;for(a in c)b._once_expired(a,c)&&(delete c[a],
7
+ e++);0<e&&f.setItem(k,JSON.stringify(c))})});d.prototype._once_expired=function(b,a){if(!a||!a.hasOwnProperty(b)||"object"!==typeof a[b])return!0;var c=a[b].ttl||u,e=(new Date).getTime();return a[b].timestamp<e-c};d.prototype._localStorageChanged=function(b,a){if(b&&b.key)return b.key===a;var c=f.getItem(a);if(c===this.previousValues[a])return!1;this.previousValues[a]=c;return!0};d.prototype._onStorageEvent=function(b){var b=b||window.event,a=this;this._localStorageChanged(b,j)&&this._transaction(function(){for(var b=
8
+ (new Date).getTime(),e=f.getItem(j),e=JSON.parse(e||"[]"),d=0;d<e.length;d++)if(e[d].origin!==a.origin&&!(e[d].timestamp<a.lastMessage)){if(e[d].id){if(a.receivedIDs.hasOwnProperty(e[d].id))continue;a.receivedIDs[e[d].id]=!0}a.trigger(e[d].name,e[d].payload)}a.lastMessage=b});this._trigger("storage",b)};d.prototype._emit=function(b,a,c){if((c="string"===typeof c||"number"===typeof c?String(c):null)&&c.length){if(this.receivedIDs.hasOwnProperty(c))return;this.receivedIDs[c]=!0}var e={id:c,name:b,origin:this.origin,
9
+ timestamp:(new Date).getTime(),payload:a},d=this;this._transaction(function(){var c=f.getItem(j)||"[]",c=[c.substring(0,c.length-1),"[]"===c?"":",",JSON.stringify(e),"]"].join("");f.setItem(j,c);d.trigger(b,a);window.setTimeout(function(){d._cleanup_emit()},50)})};d.prototype.bind=function(b,a){for(var c=0;c<d.bindings.length;c++){var e=d.bindings[c].factory(b,a||null,this);e&&this.bindings.push(e)}};d.prototype.emit=function(b,a){this._emit.apply(this,arguments);this._trigger("emit",b,a)};d.prototype.once=
10
+ function(b,a,c){if(d.supported){var e=this;this._transaction(function(){var d=JSON.parse(f.getItem(k)||"{}");e._once_expired(b,d)&&(d[b]={},d[b].timestamp=(new Date).getTime(),"number"===typeof c&&(d[b].ttl=1E3*c),f.setItem(k,JSON.stringify(d)),a(),window.setTimeout(function(){e._cleanup_once()},50))})}};i.extend(d.prototype,g.prototype);d.bindings=[];d.supported="undefined"!==typeof f;var j="intercom",k="intercom_once",l="intercom_lock",t=5E4,u=36E5;d.destroy=function(){f.removeItem(l);f.removeItem(j);
11
+ f.removeItem(k)};var q=null;d.getInstance=function(){q||(q=new d);return q};var r=function(b,a,c){a=i.extend({id:null,send:!0,receive:!0},a);if(a.receive){var d=[],f=function(f){-1===d.indexOf(f)&&(d.push(f),b.on(f,function(b){var d="function"===typeof a.id?a.id(f,b):null,e="function"===typeof a.receive?a.receive(f,b):!0;(e||"boolean"!==typeof e)&&c._emit(f,b,d)}))},g;for(g in c.handlers)for(var h=0;h<c.handlers[g].length;h++)f(g,c.handlers[g][h]);c._on("on",f)}a.send&&c._on("emit",function(c,d){var e=
12
+ "function"===typeof a.send?a.send(c,d):!0;(e||"boolean"!==typeof e)&&b.emit(c,d)})};r.factory=function(b,a,c){return"undefined"===typeof b.socket?!1:new r(b,a,c)};d.bindings.push(r);return d}();
trunk/assets/js/product-customizer.js ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** Define class */
2
+ var Printful_Product_Customizer;
3
+
4
+ (function () {
5
+ 'use strict';
6
+
7
+ /**
8
+ *
9
+ * @type {{modal: null, baseUrl: string, onCustomizeClick: onCustomizeClick, closeModal: closeModal, createModal: (function(string): HTMLDivElement), listenForResponse: listenForResponse, onCustomDesignSave: onCustomDesignSave}}
10
+ */
11
+ Printful_Product_Customizer = {
12
+ modal: null,
13
+ baseUrl: 'https://www.printful.com',
14
+
15
+ /**
16
+ * Handle click event
17
+ * @param {string} site_url
18
+ */
19
+ onCustomizeClick: function (site_url) {
20
+ var variation_id = jQuery('input[name="variation_id"]').val(); // get selected variation ID
21
+
22
+ if (!variation_id) {
23
+ // if variation does not exist, probably means single variant product
24
+ variation_id = jQuery('button[name="add-to-cart"]').val();
25
+ }
26
+
27
+ if (!variation_id) {
28
+ return;
29
+ }
30
+
31
+ var url = this.baseUrl + '/product-customizer/?website=' + site_url + '&variant=' + variation_id;
32
+
33
+ document.body.appendChild(this.createModal(url));
34
+ //start listening to post messaged
35
+ this.listenForResponse();
36
+ },
37
+
38
+ /**
39
+ * Close the modal element
40
+ */
41
+ closeModal: function () {
42
+ if (this.modal) {
43
+ this.modal.parentNode.removeChild(this.modal);
44
+ this.modal = null;
45
+ }
46
+ },
47
+
48
+ /**
49
+ * Create modal content
50
+ * @param {string} url
51
+ * @returns {HTMLDivElement}
52
+ */
53
+ createModal: function (url) {
54
+ // clear the old one just to be sure
55
+ this.closeModal();
56
+
57
+ // create iframe
58
+ var isMobileSafari = false;
59
+ var userAgent = (window.navigator && window.navigator.userAgent) ? window.navigator.userAgent : false;
60
+
61
+ if (userAgent && userAgent.match(/iPhone|iPad|iPod/i)) {
62
+ isMobileSafari = true;
63
+ }
64
+
65
+ var modal = document.createElement('div');
66
+ modal.className = 'pf-customize-modal';
67
+ modal.setAttribute('style', 'position: fixed; z-index: 2147483648; padding: 20px; top: 0; width: 100%; height: 100%; left: 0; background: rgba(0, 0 , 0, 0.6); box-sizing: border-box;');
68
+
69
+
70
+ var modalHeader = document.createElement('div');
71
+ modalHeader.setAttribute('style', 'padding: 15px; border-bottom: 1px solid #e5e5e5; overflow:hidden; position:absolute; top:0; left:0; width:100%; box-sizing: border-box;');
72
+
73
+ var closeBtn = document.createElement('button');
74
+ closeBtn.onclick = this.closeModal.bind(this);
75
+ closeBtn.setAttribute('style', 'height:30px; width:30px; cursor:pointer; border:0px; background:0 0; padding:0; -webkit-appearance:none; color:#000; float:right; background:none;');
76
+ var closeImg = document.createElement('img');
77
+ closeImg.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAAXNSR0IArs4c6QAAAgVJREFUaAXtmU2KwkAQhZ0hQbyDu7mIB3PhwoN5EXfeIYguJo9JQRMydurnNYxTDTH+dNWr71VD2mSzyZEOpAPpQDqQDqQD6UA6kA6kA80dOJ1O++Px+NVKGFrQ9Oh9WoMhPAzD5fF4XFpAQwNa0PRAf1iABXaMle7e+r4/nM/nqyVfLUZgx3nS3etutzuMddxqsfPf1cALsJKTAr0AK3omaPWSfj6f21ERx3zso5f3C1hob6da5nW8/KzuMLJVCgnpNEvDBMyGZsGibjMwC5oJ6waOhmbDhgBHQbeADQP2QreCDQW2QreEDQfWQreGpQCvhcY8bFTGk2wX8ZWMkGu5JCvPrstSmWj+vta9aX5TWGjSgJG8Ao0p80HrrAhRgSGigKbDoh468EroJrCoRf1vCUF/edA7/K+WtAJWFg19adM6XIGVWzPvcVmqweL+F1r6FhuPNbBys08zV9a89xy6pC0AlhgPdBiwp3BPrBY+BDii4Igca+DdwJGFRub6Dd4FzCiQkbOENwMzC2PmNgEzC5JusDTUwKxCBLQ8M7TU/5a6rruPReGYj/B9MDYo065MtqKl5n2qpfyu+l7dYWRceIIYDltWvtBp05ND5DQBI7CA3jKfDUMLo4C+W58N/2RyvAIahThSqEKhBU1VUE5OB9KBdCAdSAfSgXQgHUgH0oEQB74BG1sUIwNoL3cAAAAASUVORK5CYII=';
78
+ closeImg.setAttribute('style', 'height:30px; width:30px;');
79
+ closeBtn.appendChild(closeImg);
80
+ modalHeader.appendChild(closeBtn);
81
+
82
+ var modalHeaderTitle = document.createElement('h4');
83
+ modalHeaderTitle.className = 'product-customizer__header-title';
84
+ modalHeaderTitle.setAttribute('style', 'float: left;font-weight:bold;font-size:23px;color:#222;line-height:30px;margin:0px;clear:none;');
85
+ modalHeaderTitle.innerText = window.pfGlobalCustomizer && window.pfGlobalCustomizer.modal_title ? window.pfGlobalCustomizer.modal_title : 'Create a personalized design';
86
+ modalHeader.appendChild(modalHeaderTitle);
87
+
88
+ var modalContent = document.createElement('div');
89
+ modalContent.setAttribute('style', 'background-color: #fff; width: 100%; height: 100%;overflow:hidden;position:relative');
90
+ modalContent.appendChild(modalHeader);
91
+
92
+ var styles = document.createElement('style');
93
+ styles.innerHTML = '@media screen and (max-width: 768px) { .product-customizer__header-title {font-size: 16px !important;} }';
94
+ modalContent.appendChild(styles);
95
+
96
+ modal.appendChild(modalContent);
97
+
98
+ var iframe = document.createElement('iframe');
99
+ iframe.src = url;
100
+ iframe.width = '100%';
101
+ iframe.height = '100%';
102
+
103
+ if (isMobileSafari) {
104
+ var iframeWrapper = document.createElement('div');
105
+ iframeWrapper.setAttribute('style', '-webkit-overflow-scrolling: touch; overflow: scroll; height: 100%; top: 61px; box-sizing: border-box; position: absolute; width: 100%; padding-bottom: 60px;');
106
+ iframe.setAttribute('style', 'border: 0; box-sizing: border-box;');
107
+ iframeWrapper.appendChild(iframe);
108
+ modalContent.appendChild(iframeWrapper);
109
+ } else {
110
+ iframe.setAttribute('style', 'border: 0; padding-top: 60px; box-sizing: border-box;');
111
+ modalContent.appendChild(iframe);
112
+ }
113
+
114
+
115
+ this.modal = modal;
116
+
117
+ return modal;
118
+ },
119
+
120
+ /**
121
+ * Listen for response from PF
122
+ */
123
+ listenForResponse: function () {
124
+ var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
125
+ var eventer = window[eventMethod];
126
+ var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
127
+ eventer(messageEvent, function (e) {
128
+ if (e.origin !== this.baseUrl) {
129
+ return;
130
+ }
131
+
132
+ if (typeof e.data['action'] === 'undefined' || e.data['action'] !== 'PFProductCustomized') {
133
+ return;
134
+ }
135
+
136
+ this.onCustomDesignSave(e.data['hash']);
137
+ }.bind(this));
138
+ },
139
+
140
+ /**
141
+ * Manage response from PF
142
+ * @param {string} hash
143
+ */
144
+ onCustomDesignSave: function (hash) {
145
+ jQuery('#pfc_hash').val(hash);
146
+ this.closeModal();
147
+
148
+ jQuery('.single_add_to_cart_button').click();
149
+ }
150
+ };
151
+ })();
152
+
153
+ setInterval(function () {
154
+ /**
155
+ * @type {Array<HTMLImageElement>}
156
+ */
157
+ var toCheck = [];
158
+
159
+ jQuery('img.pf-image-pending').each(function () {
160
+ jQuery(this).removeClass('pf-image-pending');
161
+ jQuery(this).addClass('pf-image-checking');
162
+ toCheck.push(this);
163
+ });
164
+
165
+ var hashes = toCheck.reduce(function(carry, image){
166
+ carry[image.getAttribute('data-hash')] = image;
167
+
168
+ return carry;
169
+ }, {});
170
+
171
+ // Resolve admin base URL
172
+ var adminURL = window.pfGlobalCustomizer && window.pfGlobalCustomizer.admin_url && window.pfGlobalCustomizer.admin_url.length
173
+ ? window.pfGlobalCustomizer.admin_url
174
+ : '/wp-admin/';
175
+
176
+ // rtrim('/') + '/'
177
+ var hasSlash = adminURL.substring(adminURL.length - 1) === '/';
178
+ if (!hasSlash) {
179
+ adminURL += '/';
180
+ }
181
+
182
+ // if pending (loading) images exist, request image urls
183
+ if (Object.keys(hashes).length > 0) {
184
+ jQuery.ajax({
185
+ url: adminURL + 'admin-ajax.php',
186
+ type: 'GET',
187
+ data: {
188
+ action: 'printful_customized_thumb',
189
+ hashes: Object.keys(hashes)
190
+ },
191
+ success: function (response) {
192
+ var result = JSON.parse(response);
193
+ for (var hash in result){
194
+ var image = hashes[hash];
195
+
196
+ if (result[hash]) {
197
+ image.src = result[hash];
198
+ jQuery(image).removeClass('pf-image-checking');
199
+ } else {
200
+ jQuery(image).removeClass('pf-image-checking');
201
+ jQuery(image).addClass('pf-image-pending');
202
+ }
203
+ }
204
+ }
205
+ });
206
+ }
207
+ }, 2000);
trunk/assets/js/product-size-guide.js ADDED
@@ -0,0 +1,468 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** Define class */
2
+ var Printful_Product_Size_Guide;
3
+
4
+ (function () {
5
+ 'use strict';
6
+
7
+ /**
8
+ * @type {{modal: null, onSizeGuideClick: onSizeGuideClick, closeModal: closeModal, createModal: (function(string): HTMLDivElement)}}
9
+ */
10
+ Printful_Product_Size_Guide = {
11
+ modal: null,
12
+ modalContentNode: null,
13
+ modalBodyNode: null,
14
+ sizeChartNode: null,
15
+ /**
16
+ * @var {{}}
17
+ * @var {Array<string>} sizeGuideData.availableSizes
18
+ * @var {{}} sizeGuideData.modelMeasurements
19
+ * @var {{}} sizeGuideData.productMeasurements
20
+ */
21
+ sizeGuideData: null,
22
+
23
+ /**
24
+ * Key is unit identifier (e.g. 'inch') value is translated unit name (e.g. 'Inches')
25
+ * @var {{}}
26
+ */
27
+ translatedUnitNames: null,
28
+
29
+ /**
30
+ * Handle click event
31
+ */
32
+ onSizeGuideClick: function () {
33
+ if (!window.pfGlobal || !window.pfGlobal.sg_data_raw) {
34
+ return;
35
+ }
36
+ this.sizeGuideData = JSON.parse(window.pfGlobal.sg_data_raw);
37
+
38
+ this.translatedUnitNames = {};
39
+ if (window.pfGlobal && window.pfGlobal.sg_unit_translations) {
40
+ this.translatedUnitNames = JSON.parse(window.pfGlobal.sg_unit_translations);
41
+ }
42
+ document.body.appendChild(this.createModal());
43
+ },
44
+
45
+ /**
46
+ * Close the modal element
47
+ */
48
+ closeModal: function () {
49
+ this.removeNode(this.sizeChartNode);
50
+ this.removeNode(this.modalContentNode);
51
+ this.removeNode(this.modalBodyNode);
52
+ this.removeNode(this.modal);
53
+ this.sizeChartNode = null;
54
+ this.modalContentNode = null;
55
+ this.modalBodyNode = null;
56
+ this.modal = null;
57
+ },
58
+
59
+ /**
60
+ * Create modal content
61
+ * @returns {HTMLDivElement}
62
+ */
63
+ createModal: function () {
64
+ // Clear the old one just to be sure
65
+ this.closeModal();
66
+ this.modal = this.buildEl('div', 'pf-size-guide-modal-wrapper');
67
+ this.modalBodyNode = this.buildEl('div', 'pf-size-guide-modal');
68
+ this.modalBodyNode.style.color = this.getTextColor();
69
+ this.modalBodyNode.style.backgroundColor = this.getBackGroundColor();
70
+ this.modalBodyNode.appendChild(this.buildModalHeader());
71
+
72
+ // Render initial active tab
73
+ this.renderModalContent(Printful_Product_Size_Guide.TAB_TYPE_PERSON);
74
+ this.modal.appendChild(this.modalBodyNode);
75
+
76
+ return this.modal;
77
+ },
78
+
79
+ buildEl: function (tagName, className, attributes) {
80
+ attributes = attributes || {};
81
+ var el = document.createElement(tagName);
82
+ if (className) {
83
+ el.className = className;
84
+ }
85
+
86
+ if (attributes.id) {
87
+ el.setAttribute('id', attributes.id);
88
+ }
89
+
90
+ if (attributes.innerHTML) {
91
+ el.innerHTML = attributes.innerHTML;
92
+ }
93
+
94
+ if (attributes.src) {
95
+ el.setAttribute('src', attributes.src);
96
+ }
97
+
98
+ if (attributes.onclick) {
99
+ el.onclick = attributes.onclick;
100
+ }
101
+
102
+ if (['td', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'].indexOf(tagName) > -1) {
103
+ el.style.backgroundColor = this.getBackGroundColor();
104
+ el.style.color = this.getTextColor();
105
+ }
106
+
107
+ return el;
108
+ },
109
+
110
+ removeNode: function (node) {
111
+ if (node && node.parentNode && node.parentNode.tagName) {
112
+ node.parentNode.removeChild(node);
113
+ }
114
+ },
115
+
116
+ renderModalContent: function (activeTabType) {
117
+ this.removeNode(this.modalContentNode);
118
+ this.modalContentNode = this.buildEl('div', 'pf-size-guide-modal__content');
119
+
120
+ var tabData = this.getSizeGuideDataForTab(activeTabType),
121
+ tabTitle = this.getTabTitle(activeTabType);
122
+
123
+ this.modalContentNode.appendChild(this.buildSizeGuideTabsNode(activeTabType));
124
+ this.modalContentNode.appendChild(this.buildTabContent(tabData, tabTitle));
125
+
126
+ this.modalBodyNode.appendChild(this.modalContentNode);
127
+ },
128
+
129
+ getSizeGuideDataForTab: function (tabType) {
130
+ if (tabType === Printful_Product_Size_Guide.TAB_TYPE_PERSON) {
131
+ return this.sizeGuideData.modelMeasurements;
132
+ }
133
+
134
+ if (tabType === Printful_Product_Size_Guide.TAB_TYPE_PRODUCT) {
135
+ return this.sizeGuideData.productMeasurements;
136
+ }
137
+
138
+ return {};
139
+ },
140
+
141
+ getTabTitle: function (tabType) {
142
+ if (tabType === Printful_Product_Size_Guide.TAB_TYPE_PERSON) {
143
+ return window.pfGlobal && window.pfGlobal.sg_tab_title_person ? window.pfGlobal.sg_tab_title_person : 'Measure yourself';
144
+ }
145
+
146
+ return window.pfGlobal && window.pfGlobal.sg_tab_title_product ? window.pfGlobal.sg_tab_title_product : 'Product measurements';
147
+ },
148
+
149
+ renderSizeChart: function (measurementData, selectedUnit) {
150
+ if (this.sizeChartNode) {
151
+ this.removeNode(this.sizeChartNode.firstChild);
152
+ } else {
153
+ this.sizeChartNode = this.buildEl('div', 'pf-size-guide-modal-size-chart');
154
+ }
155
+
156
+ this.sizeChartNode.appendChild(this.buildSizeChartBlock(measurementData, selectedUnit));
157
+ },
158
+
159
+ buildModalHeader: function () {
160
+ var title = window.pfGlobal && window.pfGlobal.sg_modal_title ? window.pfGlobal.sg_modal_title : 'Size guide',
161
+ wrapper = this.buildEl('div', 'pf-size-guide-modal__header');
162
+
163
+ wrapper.appendChild(this.buildEl('h4', 'pf-size-guide-modal__title', {innerHTML: title}));
164
+
165
+ var closeBtn = this.buildEl('button', 'pf-size-guide-modal__close'),
166
+ img = this.buildEl('img');
167
+
168
+ img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAAAXNSR0IArs4c6QAAAgVJREFUaAXtmU2KwkAQhZ0hQbyDu7mIB3PhwoN5EXfeIYguJo9JQRMydurnNYxTDTH+dNWr71VD2mSzyZEOpAPpQDqQDqQD6UA6kA6kA80dOJ1O++Px+NVKGFrQ9Oh9WoMhPAzD5fF4XFpAQwNa0PRAf1iABXaMle7e+r4/nM/nqyVfLUZgx3nS3etutzuMddxqsfPf1cALsJKTAr0AK3omaPWSfj6f21ERx3zso5f3C1hob6da5nW8/KzuMLJVCgnpNEvDBMyGZsGibjMwC5oJ6waOhmbDhgBHQbeADQP2QreCDQW2QreEDQfWQreGpQCvhcY8bFTGk2wX8ZWMkGu5JCvPrstSmWj+vta9aX5TWGjSgJG8Ao0p80HrrAhRgSGigKbDoh468EroJrCoRf1vCUF/edA7/K+WtAJWFg19adM6XIGVWzPvcVmqweL+F1r6FhuPNbBys08zV9a89xy6pC0AlhgPdBiwp3BPrBY+BDii4Igca+DdwJGFRub6Dd4FzCiQkbOENwMzC2PmNgEzC5JusDTUwKxCBLQ8M7TU/5a6rruPReGYj/B9MDYo065MtqKl5n2qpfyu+l7dYWRceIIYDltWvtBp05ND5DQBI7CA3jKfDUMLo4C+W58N/2RyvAIahThSqEKhBU1VUE5OB9KBdCAdSAfSgXQgHUgH0oEQB74BG1sUIwNoL3cAAAAASUVORK5CYII=";
169
+ closeBtn.appendChild(img);
170
+ closeBtn.onclick = (this.closeModal).bind(this);
171
+
172
+ wrapper.appendChild(closeBtn);
173
+ wrapper.append(this.buildEl('div', 'pf-size-guide-modal-clear'));
174
+
175
+ return wrapper;
176
+ },
177
+
178
+ buildSizeGuideTabsNode: function (activeTabType) {
179
+ var tabs = this.buildEl('ul', 'pf-product-size-guide__tabs');
180
+ tabs.appendChild(this.buildSizeGuideTabNode(this.getTabTitle(Printful_Product_Size_Guide.TAB_TYPE_PERSON), Printful_Product_Size_Guide.TAB_TYPE_PERSON, activeTabType));
181
+ tabs.appendChild(this.buildSizeGuideTabNode(this.getTabTitle(Printful_Product_Size_Guide.TAB_TYPE_PRODUCT), Printful_Product_Size_Guide.TAB_TYPE_PRODUCT, activeTabType));
182
+
183
+ return tabs;
184
+ },
185
+
186
+ buildSizeGuideTabNode: function (title, type, activeType) {
187
+ var className = 'pf-product-size-guide__tab';
188
+
189
+ if (type === activeType) {
190
+ className += ' pf-product-size-guide__tab--active';
191
+ }
192
+
193
+ var tab = this.buildEl('li', className, {
194
+ innerHTML: title,
195
+ onclick: (this.renderModalContent).bind(this, type)
196
+ });
197
+
198
+ if (type === activeType) {
199
+ tab.style.backgroundColor = this.getTabBackGroundColor(true);
200
+ } else {
201
+ tab.style.backgroundColor = this.getTabBackGroundColor(false);
202
+ }
203
+
204
+ return tab;
205
+ },
206
+
207
+ buildTabContent: function (measurementData, title) {
208
+ measurementData = measurementData || {};
209
+ var node = this.buildEl('div');
210
+ node.appendChild(this.buildEl('h4', null, {innerHTML: title}));
211
+ if (measurementData.hasOwnProperty('description')) {
212
+ node.appendChild(this.buildEl('div', null, {innerHTML: measurementData.description}));
213
+ }
214
+
215
+ // Model description
216
+ node.appendChild(this.buildDescriptionBlock(measurementData));
217
+
218
+ // Size table
219
+ this.renderSizeChart(measurementData);
220
+ node.appendChild(this.sizeChartNode);
221
+
222
+ return node;
223
+ },
224
+
225
+ buildDescriptionBlock: function (measurementData) {
226
+ var node = this.buildEl('div', 'pf-size-guide-modal-measurements');
227
+ if (measurementData.hasOwnProperty('imageUrl') && measurementData.imageUrl) {
228
+ var measurementImgNode = this.buildEl('div', 'pf-size-guide-modal-measurements__image');
229
+ measurementImgNode.appendChild(this.buildEl('img', null, {src: measurementData.imageUrl}));
230
+
231
+ if (measurementData.hasOwnProperty('modelDescription')) {
232
+ measurementImgNode.appendChild(this.buildEl('div', null, {innerHTML: measurementData.modelDescription}));
233
+ }
234
+ node.appendChild(measurementImgNode);
235
+ }
236
+
237
+ if (measurementData.hasOwnProperty('imageDescription')) {
238
+ node.appendChild(this.buildEl('div', 'pf-size-guide-modal-measurements__description', {innerHTML: measurementData.imageDescription}));
239
+ }
240
+ node.append(this.buildEl('div', 'pf-size-guide-modal-clear'));
241
+
242
+ return node;
243
+ },
244
+
245
+ /**
246
+ * @param {{}} measurementData
247
+ * @param {string} [selectedUnit]
248
+ * @return {*}
249
+ */
250
+ buildSizeChartBlock: function (measurementData, selectedUnit) {
251
+ var node = this.buildEl('div');
252
+
253
+ if (!measurementData.hasOwnProperty('sizeTableRows') || measurementData.sizeTableRows.length < 1) {
254
+ return node;
255
+ }
256
+ var sizeRows = measurementData.sizeTableRows,
257
+ availableUnits = this.getUniqueUnits(sizeRows);
258
+
259
+ // Selected or first if nothing selected
260
+ selectedUnit = selectedUnit || this.getDefaultUnit(availableUnits);
261
+ node.appendChild(this.buildSizeChartTabsNode(measurementData, availableUnits, selectedUnit));
262
+ var wrapper = this.buildEl('div');
263
+ wrapper.style.overflowX = 'auto';
264
+ wrapper.appendChild(this.buildSizeChartTable(this.getSortedChartRows(sizeRows, selectedUnit)));
265
+ node.appendChild(wrapper);
266
+
267
+ return node;
268
+ },
269
+
270
+ /**
271
+ * @param {{}} measurementData
272
+ * @param {Array<{key, title}>} availableUnits
273
+ * @param {string} selectedUnit
274
+ * @return {*}
275
+ */
276
+ buildSizeChartTabsNode: function (measurementData, availableUnits, selectedUnit) {
277
+ availableUnits = availableUnits || [];
278
+ var tabsNode = this.buildEl('ul', 'pf-size-guide-modal-size-chart__tabs');
279
+
280
+ availableUnits.map((function (item) {
281
+ var className = 'pf-size-guide-modal-size-chart__tab';
282
+ if (item.key === selectedUnit) {
283
+ className += ' pf-size-guide-modal-size-chart__tab--active';
284
+ }
285
+ tabsNode.appendChild(this.buildEl('li', className, {
286
+ innerHTML: item.title,
287
+ onclick: (this.renderSizeChart).bind(this, measurementData, item.key)
288
+ }));
289
+ }).bind(this));
290
+
291
+ return tabsNode;
292
+ },
293
+
294
+ buildSizeChartTable: function (rows) {
295
+ var tableNode = this.buildEl('table', 'pf-size-guide-modal-size-chart__table'),
296
+ availableSizes = this.sizeGuideData.availableSizes;
297
+
298
+ var tableHeader = this.buildEl('thead'),
299
+ tableHeaderRow = this.buildEl('tr');
300
+
301
+ var tableHeaderRowText = window.pfGlobal && window.pfGlobal.sg_table_header_size ? window.pfGlobal.sg_table_header_size : 'Size';
302
+
303
+ tableHeaderRow.appendChild(this.buildEl('td', null, {innerHTML: tableHeaderRowText}));
304
+ rows.map((function (row) {
305
+ tableHeaderRow.appendChild(this.buildEl('td', null, {innerHTML: row.title}));
306
+ }).bind(this));
307
+
308
+ tableHeader.appendChild(tableHeaderRow);
309
+ tableNode.appendChild(tableHeader);
310
+
311
+ // Loop all available sizes and print out a row for each type
312
+ var tableBody = this.buildEl('tbody');
313
+ availableSizes.map((function (size) {
314
+ var tableBodyRow = this.buildEl('tr');
315
+ tableBodyRow.appendChild(this.buildEl('td', null, {innerHTML: size}));
316
+ rows.map((function (row) {
317
+ tableBodyRow.appendChild(this.buildEl('td', null, {innerHTML: row.sizes[size] || ''}));
318
+ }).bind(this));
319
+
320
+ tableBody.appendChild(tableBodyRow);
321
+ }).bind(this));
322
+
323
+ tableNode.appendChild(tableBody);
324
+
325
+ return tableNode;
326
+ },
327
+
328
+ getSortedChartRows: function (sizeTableRows, selectedUnit) {
329
+ return sizeTableRows
330
+ .filter(function (row) {
331
+ return row.unit === selectedUnit;
332
+ }).map((function (row) {
333
+ var sizes = {};
334
+ for (var k in row.sizes) {
335
+ if (!row.sizes.hasOwnProperty(k)) {
336
+ continue;
337
+ }
338
+
339
+ // Convert to fractions if needed, round and join
340
+ sizes[k] = row.sizes[k].map((function (size) {
341
+ if (row.unit === Printful_Product_Size_Guide.UNIT_INCH) {
342
+ return this.convertToFraction(size);
343
+ } else {
344
+ return size.toFixed(1);
345
+ }
346
+ }).bind(this)).join(' - ');
347
+ }
348
+
349
+ return {
350
+ title: row.title,
351
+ sizes: sizes
352
+ };
353
+ }).bind(this));
354
+ },
355
+
356
+ convertToFraction: function (value) {
357
+ var split = String(value).split('.'),
358
+ integer = parseInt(split[0], 10);
359
+
360
+ if (!split[1]) {
361
+ return value;
362
+ }
363
+
364
+ var decimal = parseFloat('0.' + split[1]),
365
+ fraction = 0;
366
+
367
+ for (var fra in Printful_Product_Size_Guide.FRACTION_MAP) {
368
+ var compareValue = Printful_Product_Size_Guide.FRACTION_MAP[fra];
369
+
370
+ if (decimal < compareValue) {
371
+ fraction = fra;
372
+ break;
373
+ }
374
+ }
375
+
376
+ if (fraction === 0 && integer) {
377
+ integer++;
378
+ fraction = '';
379
+ }
380
+
381
+ return (integer > 0 ? integer + ' ' : '') + fraction;
382
+ },
383
+
384
+ /**
385
+ * @param {Array<{unit, unitName}>}sizeTableRows
386
+ * @return {Array<{key, title}>}
387
+ */
388
+ getUniqueUnits: function (sizeTableRows) {
389
+ var uniqueUnits = {};
390
+ sizeTableRows.map(function (row) {
391
+ // Gather unique units
392
+ if (!uniqueUnits.hasOwnProperty(row.unit)) {
393
+ uniqueUnits[row.unit] = row.unitName;
394
+ }
395
+ });
396
+
397
+ var units = [];
398
+ for (var i in uniqueUnits) {
399
+ if (uniqueUnits.hasOwnProperty(i)) {
400
+ units.push({
401
+ key: i,
402
+ title: this.getTranslatedUnitName(i, uniqueUnits[i])
403
+ });
404
+ }
405
+ }
406
+
407
+ return units;
408
+ },
409
+
410
+ /**
411
+ * @param {string} unit
412
+ * @param {string} defaultValue
413
+ * @return {null|string}
414
+ */
415
+ getTranslatedUnitName: function(unit, defaultValue) {
416
+ if (this.translatedUnitNames[unit]) {
417
+ return this.translatedUnitNames[unit];
418
+ }
419
+
420
+ return defaultValue;
421
+ },
422
+
423
+ getBackGroundColor: function () {
424
+ return window.pfGlobal && window.pfGlobal.sg_modal_background_color ? window.pfGlobal.sg_modal_background_color : '#FFF';
425
+ },
426
+
427
+ getTextColor: function () {
428
+ return window.pfGlobal && window.pfGlobal.sg_modal_text_color ? window.pfGlobal.sg_modal_text_color : '#000';
429
+ },
430
+
431
+ getTabBackGroundColor: function (isActive) {
432
+ if (isActive) {
433
+ return window.pfGlobal && window.pfGlobal.sg_active_tab_background_color ? window.pfGlobal.sg_active_tab_background_color : '#FFF';
434
+ }
435
+
436
+ return window.pfGlobal && window.pfGlobal.sg_tab_background_color ? window.pfGlobal.sg_tab_background_color : '#EEE';
437
+ },
438
+
439
+ getDefaultUnit: function (availableUnits) {
440
+ availableUnits = availableUnits || [];
441
+ var uniqueUnitTypes = availableUnits.map(function (item) {
442
+ return item.key;
443
+ });
444
+
445
+ var defaultUnit = uniqueUnitTypes[0];
446
+ if (window.pfGlobal && window.pfGlobal.sg_primary_unit && uniqueUnitTypes.indexOf(window.pfGlobal.sg_primary_unit) > -1) {
447
+ return window.pfGlobal.sg_primary_unit;
448
+ }
449
+
450
+ return defaultUnit;
451
+ }
452
+ };
453
+
454
+ Printful_Product_Size_Guide.TAB_TYPE_PERSON = 'person';
455
+ Printful_Product_Size_Guide.TAB_TYPE_PRODUCT = 'product';
456
+ Printful_Product_Size_Guide.UNIT_INCH = 'inch';
457
+
458
+ Printful_Product_Size_Guide.FRACTION_MAP = {
459
+ ' ': 0.0625, //don't add any fraction
460
+ '⅛': 0.187,
461
+ '¼': 0.3125,
462
+ '⅜': 0.4375,
463
+ '½': 0.5625,
464
+ '⅝': 0.6875,
465
+ '¾': 0.8125,
466
+ '⅞': 0.9375
467
+ };
468
+ })();
trunk/assets/js/settings.js ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var Printful_Settings;
2
+
3
+ (function () {
4
+ 'use strict';
5
+
6
+ Printful_Settings = {
7
+ init_submit: function () {
8
+
9
+ var form = jQuery('form[name=printful_settings]');
10
+ var submit_button = form.find('.woocommerce-save-button');
11
+ var loader = form.find('.loader');
12
+ var pass = form.find('.loader-wrap .pass');
13
+ var fail = form.find('.loader-wrap .fail');
14
+
15
+ submit_button.click(function (e) {
16
+
17
+ e.preventDefault();
18
+ submit_button.attr('disabled', 'disabled');
19
+ loader.show();
20
+
21
+ jQuery.ajax({
22
+ type: "POST",
23
+ url: form.attr('action'),
24
+ data: form.serialize(),
25
+ success: function (response) {
26
+ submit_button.removeAttr('disabled');
27
+ loader.hide();
28
+
29
+ if (response === 'OK') {
30
+ pass.show(0).delay(3000).hide(0);
31
+ } else {
32
+ fail.empty();
33
+ fail.append('<span class="dashicons dashicons-no"></span>' + response);
34
+ fail.show(0).delay(3000).hide(0);
35
+ }
36
+ }
37
+ });
38
+ });
39
+ },
40
+ enable_submit_btn: function () {
41
+ jQuery('.printful-submit input[type=submit]').removeClass('disabled').prop('disabled', false);
42
+ }
43
+ };
44
+ })();
trunk/i18n/languages/printful.pot ADDED
@@ -0,0 +1,695 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #, fuzzy
2
+ msgid ""
3
+ msgstr ""
4
+ "Project-Id-Version: \n"
5
+ "POT-Creation-Date: 2020-09-07 18:56+0300\n"
6
+ "PO-Revision-Date: 2018-04-26 13:50+0300\n"
7
+ "Last-Translator: \n"
8
+ "Language-Team: \n"
9
+ "Language: en\n"
10
+ "MIME-Version: 1.0\n"
11
+ "Content-Type: text/plain; charset=UTF-8\n"
12
+ "Content-Transfer-Encoding: 8bit\n"
13
+ "X-Generator: Poedit 2.2.3\n"
14
+ "X-Poedit-Basepath: ../..\n"
15
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
16
+ "X-Poedit-KeywordsList: __;_e;_n;_x;_ex;_nx;esc_attr__;esc_attr_e;esc_attr_x;"
17
+ "esc_html__;esc_html_e;esc_html_x;_n_noop;_nx_noop;translate_nooped_plural\n"
18
+ "X-Poedit-SearchPath-0: .\n"
19
+
20
+ #: includes/class-printful-admin-dashboard.php:124
21
+ msgid "Loading your stats..."
22
+ msgstr ""
23
+
24
+ #: includes/class-printful-admin-dashboard.php:130
25
+ msgid "Loading your orders..."
26
+ msgstr ""
27
+
28
+ #: includes/class-printful-admin-settings.php:35
29
+ msgid "states where Printful applies sales tax"
30
+ msgstr ""
31
+
32
+ #: includes/class-printful-admin-settings.php:40
33
+ msgid "Printful store API key"
34
+ msgstr ""
35
+
36
+ #: includes/class-printful-admin-settings.php:43
37
+ msgid "Your store's Printful API key. Create it in the Prinful dashboard"
38
+ msgstr ""
39
+
40
+ #: includes/class-printful-admin-settings.php:47
41
+ msgid "Calculate sales tax"
42
+ msgstr ""
43
+
44
+ #: includes/class-printful-admin-settings.php:50
45
+ #, php-format
46
+ msgid ""
47
+ "Calculated for all products listed on your store (including non-Printful "
48
+ "products) that ship to %s. Before enabling, make sure you are registered for "
49
+ "a seller permit in all these states."
50
+ msgstr ""
51
+
52
+ #: includes/class-printful-admin-settings.php:56
53
+ msgid "Disable SSL"
54
+ msgstr ""
55
+
56
+ #: includes/class-printful-admin-settings.php:58
57
+ msgid ""
58
+ "Use HTTP instead of HTTPS to connect to the Printful API (may be required if "
59
+ "the plugin does not work for some hosting configurations)"
60
+ msgstr ""
61
+
62
+ #: includes/class-printful-admin-settings.php:68
63
+ #: includes/class-printful-admin-settings.php:70
64
+ msgid "Personalization button text"
65
+ msgstr ""
66
+
67
+ #: includes/class-printful-admin-settings.php:74
68
+ msgid "Personalization button color"
69
+ msgstr ""
70
+
71
+ #: includes/class-printful-admin-settings.php:76
72
+ msgid "Personalization button background color"
73
+ msgstr ""
74
+
75
+ #: includes/class-printful-admin-settings.php:80
76
+ msgid "Personalization popup title"
77
+ msgstr ""
78
+
79
+ #: includes/class-printful-admin-settings.php:82
80
+ msgid "Personalization popup title text"
81
+ msgstr ""
82
+
83
+ #: includes/class-printful-admin-settings.php:92
84
+ msgid "Size guide popup title"
85
+ msgstr ""
86
+
87
+ #: includes/class-printful-admin-settings.php:94
88
+ msgid "Size guide popup title text"
89
+ msgstr ""
90
+
91
+ #: includes/class-printful-admin-settings.php:98
92
+ #: includes/class-printful-admin-settings.php:100
93
+ msgid "Size guide popup text color"
94
+ msgstr ""
95
+
96
+ #: includes/class-printful-admin-settings.php:104
97
+ #: includes/class-printful-admin-settings.php:106
98
+ msgid "Size guide popup background color"
99
+ msgstr ""
100
+
101
+ #: includes/class-printful-admin-settings.php:110
102
+ #: includes/class-printful-admin-settings.php:112
103
+ msgid "Size guide tab background color"
104
+ msgstr ""
105
+
106
+ #: includes/class-printful-admin-settings.php:116
107
+ #: includes/class-printful-admin-settings.php:118
108
+ msgid "Size guide active tab background color"
109
+ msgstr ""
110
+
111
+ #: includes/class-printful-admin-settings.php:122
112
+ #: includes/class-printful-admin-settings.php:124
113
+ msgid "Size guide button text"
114
+ msgstr ""
115
+
116
+ #: includes/class-printful-admin-settings.php:128
117
+ #: includes/class-printful-admin-settings.php:130
118
+ msgid "Size guide button text color"
119
+ msgstr ""
120
+
121
+ #: includes/class-printful-admin-settings.php:134
122
+ msgid "Primary measurement unit"
123
+ msgstr ""
124
+
125
+ #: includes/class-printful-admin-settings.php:136
126
+ msgid "Primary measurement unit (cm/inch)"
127
+ msgstr ""
128
+
129
+ #: includes/class-printful-admin-settings.php:140
130
+ msgid "Inches"
131
+ msgstr ""
132
+
133
+ #: includes/class-printful-admin-settings.php:141
134
+ msgid "Centimeters"
135
+ msgstr ""
136
+
137
+ #: includes/class-printful-admin-settings.php:185
138
+ msgid "Integration settings"
139
+ msgstr ""
140
+
141
+ #: includes/class-printful-admin-settings.php:189
142
+ msgid "Product personalization settings"
143
+ msgstr ""
144
+
145
+ #: includes/class-printful-admin-settings.php:194
146
+ msgid "Size guide settings"
147
+ msgstr ""
148
+
149
+ #: includes/class-printful-admin-settings.php:195
150
+ msgid ""
151
+ "These settings control how the new size guide will look on your WooCommerce "
152
+ "storefront. Products with an old size guide will not be affected."
153
+ msgstr ""
154
+
155
+ #: includes/class-printful-admin-settings.php:256
156
+ msgid "Shipping Methods"
157
+ msgstr ""
158
+
159
+ #: includes/class-printful-admin-settings.php:257
160
+ msgid ""
161
+ "Here you can choose the shipping methods you want Printful to use for "
162
+ "shipping your orders.\n"
163
+ " Uncheck the ones you want to disable. From your selection, our "
164
+ "algorithm will determine the fastest, most cost-effective, and most reliable "
165
+ "method for each order."
166
+ msgstr ""
167
+
168
+ #: includes/class-printful-admin-settings.php:263
169
+ msgid "You need to be connected to Printful API to edit carrier settings!"
170
+ msgstr ""
171
+
172
+ #: includes/class-printful-admin-settings.php:404
173
+ msgid "International from USA"
174
+ msgstr ""
175
+
176
+ #: includes/class-printful-admin-settings.php:410
177
+ msgid "International from EU"
178
+ msgstr ""
179
+
180
+ #: includes/class-printful-admin-status.php:29
181
+ msgid "Connection to Printful API"
182
+ msgstr ""
183
+
184
+ #: includes/class-printful-admin-status.php:30
185
+ msgid "Is your store successfully connected to Printful API?"
186
+ msgstr ""
187
+
188
+ #: includes/class-printful-admin-status.php:34
189
+ msgid "Printful API key is set"
190
+ msgstr ""
191
+
192
+ #: includes/class-printful-admin-status.php:35
193
+ msgid ""
194
+ "Your store needs access to Printful API to use most of it's features like "
195
+ "shipping rates, tax rates and other settings."
196
+ msgstr ""
197
+
198
+ #: includes/class-printful-admin-status.php:39
199
+ msgid "WordPress Permalinks"
200
+ msgstr ""
201
+
202
+ #: includes/class-printful-admin-status.php:40
203
+ msgid ""
204
+ "WooCommerce API will not work unless your permalinks in Settings > "
205
+ "Permalinks are set up correctly. Make sure you that they are NOT set to "
206
+ "\"plain\"."
207
+ msgstr ""
208
+
209
+ #: includes/class-printful-admin-status.php:44
210
+ msgid "WordPress version"
211
+ msgstr ""
212
+
213
+ #: includes/class-printful-admin-status.php:45
214
+ msgid ""
215
+ "WordPress should always be updated to the latest version. Updates can be "
216
+ "installed from your WordPress admin dashboard."
217
+ msgstr ""
218
+
219
+ #: includes/class-printful-admin-status.php:49
220
+ msgid "WooCommerce Webhooks"
221
+ msgstr ""
222
+
223
+ #: includes/class-printful-admin-status.php:50
224
+ msgid ""
225
+ "Printful requires WooCommerce webhooks to be set up to quickly capture you "
226
+ "incoming orders, products updates etc."
227
+ msgstr ""
228
+
229
+ #: includes/class-printful-admin-status.php:54
230
+ msgid "WooCommerce API keys are set"
231
+ msgstr ""
232
+
233
+ #: includes/class-printful-admin-status.php:55
234
+ msgid ""
235
+ "Printful needs access to your WooCommerce API for the integration to work - "
236
+ "otherwise we can't sync your store, push or pull your products etc."
237
+ msgstr ""
238
+
239
+ #: includes/class-printful-admin-status.php:59
240
+ msgid "WooCommerce authentication URL access"
241
+ msgstr ""
242
+
243
+ #: includes/class-printful-admin-status.php:60
244
+ msgid ""
245
+ "Printful needs access to WooCommerce API authorize page. This sometimes may "
246
+ "get blocked due to hosts having unnecessarily intrusive security checks in "
247
+ "place that prevent WooCommerce API authentication from working (for example "
248
+ "mod_security rule #1234234). If this check fails, you will not be able "
249
+ "authorize Printful app."
250
+ msgstr ""
251
+
252
+ #: includes/class-printful-admin-status.php:64
253
+ msgid "WordPress remote requests"
254
+ msgstr ""
255
+
256
+ #: includes/class-printful-admin-status.php:65
257
+ msgid ""
258
+ "WordPress needs to be able to connect to Printful server to call webhooks. "
259
+ "If this check fails, contact your hosting support."
260
+ msgstr ""
261
+
262
+ #: includes/class-printful-admin-status.php:69
263
+ msgid "WordPress Site URL"
264
+ msgstr ""
265
+
266
+ #: includes/class-printful-admin-status.php:70
267
+ msgid ""
268
+ "If your currently setup WordPress site URL is redirected to another URL the "
269
+ "integration might not work correctly. Typically this happens with incorrect "
270
+ "http to https redirects. Go to Settings > General to fix this."
271
+ msgstr ""
272
+
273
+ #: includes/class-printful-admin-status.php:74
274
+ msgid "Recent store sync errors"
275
+ msgstr ""
276
+
277
+ #: includes/class-printful-admin-status.php:75
278
+ msgid ""
279
+ "Printful will connect to your store's API regularly and sync your latest "
280
+ "products, orders etc. If there have been any recent issues with sync, this "
281
+ "check will fail."
282
+ msgstr ""
283
+
284
+ #: includes/class-printful-admin-status.php:79
285
+ msgid "Write permissions"
286
+ msgstr ""
287
+
288
+ #: includes/class-printful-admin-status.php:80
289
+ msgid ""
290
+ "Make the uploads directory writable. This is required for mockup generator "
291
+ "product push to work correctly. Contact your hosting provider if you need "
292
+ "help with this."
293
+ msgstr ""
294
+
295
+ #: includes/class-printful-admin-status.php:84
296
+ msgid "PHP memory limit"
297
+ msgstr ""
298
+
299
+ #: includes/class-printful-admin-status.php:85
300
+ msgid ""
301
+ "Set PHP allocated memory limit to at least 128mb. Contact your hosting "
302
+ "provider if you need help with this."
303
+ msgstr ""
304
+
305
+ #: includes/class-printful-admin-status.php:89
306
+ msgid "PHP script time limit"
307
+ msgstr ""
308
+
309
+ #: includes/class-printful-admin-status.php:90
310
+ msgid ""
311
+ "Set PHP script execution time limit to at least 30 seconds. This is required "
312
+ "to successfully push products with many variants. Contact your hosting "
313
+ "provider if you need help with this."
314
+ msgstr ""
315
+
316
+ #: includes/class-printful-admin-status.php:94
317
+ msgid "W3 Total Cache DB Cache"
318
+ msgstr ""
319
+
320
+ #: includes/class-printful-admin-status.php:95
321
+ msgid ""
322
+ "If you are using W3 Total Cache, the database caching feature needs to be "
323
+ "disabled since it can cause issues with product push to store."
324
+ msgstr ""
325
+
326
+ #: includes/class-printful-admin-status.php:100
327
+ msgid "WP SpamShield"
328
+ msgstr ""
329
+
330
+ #: includes/class-printful-admin-status.php:101
331
+ msgid ""
332
+ "If you are using WP SpamShield, you might experience problems connecting to "
333
+ "Printful and pushing products."
334
+ msgstr ""
335
+
336
+ #: includes/class-printful-admin-status.php:106
337
+ msgid "Remove Print Aura plugin"
338
+ msgstr ""
339
+
340
+ #: includes/class-printful-admin-status.php:107
341
+ msgid "Print Aura plugin is known to cause issues so it needs to be removed."
342
+ msgstr ""
343
+
344
+ #: includes/class-printful-admin-status.php:148
345
+ msgid "Testing your store (this may take up to 30 seconds)..."
346
+ msgstr ""
347
+
348
+ #: includes/class-printful-admin-support.php:36
349
+ msgid "Building support report (this may take up to 30 seconds)..."
350
+ msgstr ""
351
+
352
+ #: includes/class-printful-admin-support.php:69
353
+ msgid "##### Printful Checklist #####\n"
354
+ msgstr ""
355
+
356
+ #: includes/class-printful-admin.php:75 includes/class-printful-admin.php:117
357
+ msgid "Dashboard"
358
+ msgstr ""
359
+
360
+ #: includes/class-printful-admin.php:111
361
+ msgid "Settings"
362
+ msgstr ""
363
+
364
+ #: includes/class-printful-admin.php:112 includes/templates/order-table.php:12
365
+ #: includes/templates/order-table.php:58 includes/templates/status-table.php:21
366
+ #: includes/templates/status-table.php:51
367
+ msgid "Status"
368
+ msgstr ""
369
+
370
+ #: includes/class-printful-admin.php:113
371
+ msgid "Support"
372
+ msgstr ""
373
+
374
+ #: includes/class-printful-admin.php:119 includes/templates/connect.php:29
375
+ msgid "Connect"
376
+ msgstr ""
377
+
378
+ #: includes/class-printful-admin.php:147
379
+ msgid " issue"
380
+ msgstr ""
381
+
382
+ #: includes/class-printful-rest-api-controller.php:36
383
+ msgid "Printful access key"
384
+ msgstr ""
385
+
386
+ #: includes/class-printful-rest-api-controller.php:41
387
+ msgid "Store Identifier"
388
+ msgstr ""
389
+
390
+ #: includes/class-printful-rest-api-controller.php:55
391
+ #: includes/class-printful-rest-api-controller.php:75
392
+ msgid "Unique identifier for the resource."
393
+ msgstr ""
394
+
395
+ #: includes/class-printful-rest-api-controller.php:61
396
+ msgid "Printful size guide"
397
+ msgstr ""
398
+
399
+ #: includes/class-printful-rest-api-controller.php:81
400
+ msgid "Advanced Printful size guide"
401
+ msgstr ""
402
+
403
+ #: includes/class-printful-rest-api-controller.php:142
404
+ msgid "No size chart was provided"
405
+ msgstr ""
406
+
407
+ #: includes/class-printful-rest-api-controller.php:148
408
+ msgid "The product ID is invalid"
409
+ msgstr ""
410
+
411
+ #: includes/class-printful-rest-api-controller.php:155
412
+ msgid "The product is not found"
413
+ msgstr ""
414
+
415
+ #: includes/class-printful-rest-api-controller.php:161
416
+ msgid "You do not have permission to edit the size chart"
417
+ msgstr ""
418
+
419
+ #: includes/class-printful-rest-api-controller.php:241
420
+ msgid "Sorry, you cannot list resources."
421
+ msgstr ""
422
+
423
+ #: includes/class-printful-rest-api-controller.php:258
424
+ msgid "Sorry, you are not allowed to edit this resource."
425
+ msgstr ""
426
+
427
+ #: includes/class-printful-shipping.php:51
428
+ msgid "Enable/Disable"
429
+ msgstr ""
430
+
431
+ #: includes/class-printful-shipping.php:53
432
+ msgid "Enable this shipping method"
433
+ msgstr ""
434
+
435
+ #: includes/class-printful-shipping.php:57
436
+ msgid "Disable Woocommerce rates"
437
+ msgstr ""
438
+
439
+ #: includes/class-printful-shipping.php:59
440
+ msgid "Disable standard Woocommerce rates for products fulfilled by Printful"
441
+ msgstr ""
442
+
443
+ #: includes/class-printful-shipping.php:63
444
+ msgid "Show Printful warnings"
445
+ msgstr ""
446
+
447
+ #: includes/class-printful-shipping.php:65
448
+ msgid "Display Printful status messages if rate API request fails"
449
+ msgstr ""
450
+
451
+ #: includes/class-printful-size-chart-tab.php:32
452
+ msgid "Size chart"
453
+ msgstr ""
454
+
455
+ #: includes/class-printful-size-chart-tab.php:43
456
+ #: includes/class-printful-size-chart-tab.php:56
457
+ msgid "Size Chart"
458
+ msgstr ""
459
+
460
+ #: includes/class-printful-size-guide.php:51
461
+ msgid "Measure yourself"
462
+ msgstr ""
463
+
464
+ #: includes/class-printful-size-guide.php:52
465
+ msgid "Product measurements"
466
+ msgstr ""
467
+
468
+ #: includes/class-printful-size-guide.php:53
469
+ msgid "Size"
470
+ msgstr ""
471
+
472
+ #: includes/class-printful-taxes.php:160
473
+ msgid "Sales Tax"
474
+ msgstr ""
475
+
476
+ #: includes/templates/connect.php:5
477
+ msgid "Connect to Printful"
478
+ msgstr ""
479
+
480
+ #: includes/templates/connect.php:12
481
+ msgid "To connect your store to Printful, fix the following errors:"
482
+ msgstr ""
483
+
484
+ #: includes/templates/connect.php:25
485
+ msgid ""
486
+ "You're almost done! Just 2 more steps to have your WooCommerce store "
487
+ "connected to Printful for automatic order fulfillment."
488
+ msgstr ""
489
+
490
+ #: includes/templates/error.php:1
491
+ msgid "Error:"
492
+ msgstr ""
493
+
494
+ #: includes/templates/order-table.php:9 includes/templates/order-table.php:55
495
+ msgid "Order"
496
+ msgstr ""
497
+
498
+ #: includes/templates/order-table.php:10 includes/templates/order-table.php:56
499
+ msgid "Date"
500
+ msgstr ""
501
+
502
+ #: includes/templates/order-table.php:11 includes/templates/order-table.php:57
503
+ msgid "From"
504
+ msgstr ""
505
+
506
+ #: includes/templates/order-table.php:13 includes/templates/order-table.php:59
507
+ msgid "Total"
508
+ msgstr ""
509
+
510
+ #: includes/templates/order-table.php:14 includes/templates/order-table.php:60
511
+ msgid "Actions"
512
+ msgstr ""
513
+
514
+ #: includes/templates/order-table.php:46
515
+ msgid "Open in Printful"
516
+ msgstr ""
517
+
518
+ #: includes/templates/order-table.php:67
519
+ msgid ""
520
+ "Once your store gets some Printful product orders, they will be shown here!"
521
+ msgstr ""
522
+
523
+ #: includes/templates/quick-links.php:6
524
+ msgid "Orders"
525
+ msgstr ""
526
+
527
+ #: includes/templates/quick-links.php:10
528
+ msgid "File library"
529
+ msgstr ""
530
+
531
+ #: includes/templates/quick-links.php:14
532
+ msgid "Stores"
533
+ msgstr ""
534
+
535
+ #: includes/templates/quick-links.php:18
536
+ msgid "Reports"
537
+ msgstr ""
538
+
539
+ #: includes/templates/quick-links.php:22
540
+ msgid "My Account"
541
+ msgstr ""
542
+
543
+ #: includes/templates/quick-links.php:26
544
+ msgid "Billing"
545
+ msgstr ""
546
+
547
+ #: includes/templates/quick-links.php:30
548
+ msgid "Notifications"
549
+ msgstr ""
550
+
551
+ #: includes/templates/setting-submit.php:7
552
+ msgid "Save changes"
553
+ msgstr ""
554
+
555
+ #: includes/templates/setting-submit.php:14
556
+ msgid "Saved successfully"
557
+ msgstr ""
558
+
559
+ #: includes/templates/shipping-notification.php:2
560
+ msgid "Printful Shipping"
561
+ msgstr ""
562
+
563
+ #: includes/templates/shipping-notification.php:3
564
+ msgid "To enable/disable Printful shipping for your store go to"
565
+ msgstr ""
566
+
567
+ #: includes/templates/shipping-notification.php:3
568
+ msgid "WooCommerce Shipping settings"
569
+ msgstr ""
570
+
571
+ #: includes/templates/stats.php:7 includes/templates/stats.php:20
572
+ #: includes/templates/stats.php:33
573
+ msgid "ORDER"
574
+ msgstr ""
575
+
576
+ #: includes/templates/stats.php:10
577
+ msgid "today"
578
+ msgstr ""
579
+
580
+ #: includes/templates/stats.php:23
581
+ msgid "last 7 days"
582
+ msgstr ""
583
+
584
+ #: includes/templates/stats.php:35 includes/templates/stats.php:42
585
+ msgid "last 28 days"
586
+ msgstr ""
587
+
588
+ #: includes/templates/stats.php:42
589
+ msgid "PROFIT"
590
+ msgstr ""
591
+
592
+ #: includes/templates/status-report.php:3
593
+ msgid "Copy the box content below and add it to your support message"
594
+ msgstr ""
595
+
596
+ #: includes/templates/status-report.php:5
597
+ msgid ""
598
+ "Note: this status report may not include an error log. Contact your hosting "
599
+ "provider if you need help with acquiring error logs."
600
+ msgstr ""
601
+
602
+ #: includes/templates/status-report.php:8
603
+ msgid "Copy"
604
+ msgstr ""
605
+
606
+ #: includes/templates/status-table.php:4
607
+ msgid ""
608
+ "Looks like the everything is set up correctly and Printful integration "
609
+ "should work as intended."
610
+ msgstr ""
611
+
612
+ #: includes/templates/status-table.php:10
613
+ msgid ""
614
+ "There are errors with your store setup that may cause the Printful "
615
+ "integration to not work as intended!"
616
+ msgstr ""
617
+
618
+ #: includes/templates/status-table.php:19
619
+ #: includes/templates/status-table.php:49
620
+ msgid "Name"
621
+ msgstr ""
622
+
623
+ #: includes/templates/status-table.php:20
624
+ #: includes/templates/status-table.php:50
625
+ msgid "Description"
626
+ msgstr ""
627
+
628
+ #: includes/templates/status-table.php:34
629
+ msgid "OK"
630
+ msgstr ""
631
+
632
+ #: includes/templates/status-table.php:36
633
+ msgid "WARNING"
634
+ msgstr ""
635
+
636
+ #: includes/templates/status-table.php:38
637
+ msgid "NOT CONNECTED"
638
+ msgstr ""
639
+
640
+ #: includes/templates/status-table.php:40
641
+ msgid "FAIL"
642
+ msgstr ""
643
+
644
+ #: includes/templates/status-table.php:56
645
+ msgid ""
646
+ "Warnings are issued when the test was unable to come to a definite "
647
+ "conclusion or if the result was passable, but not ideal."
648
+ msgstr ""
649
+
650
+ #: includes/templates/support-info.php:4
651
+ msgid "Need help? Get in touch!"
652
+ msgstr ""
653
+
654
+ #: includes/templates/support-info.php:5
655
+ msgid ""
656
+ "Email, call, text, or visit us. We've got support teams in the USA and "
657
+ "Europe to make sure we're there when you need us."
658
+ msgstr ""
659
+
660
+ #: includes/templates/support-info.php:5
661
+ msgid "We'd love to hear from you."
662
+ msgstr ""
663
+
664
+ #: includes/templates/support-info.php:7
665
+ msgid "Contact support"
666
+ msgstr ""
667
+
668
+ #: includes/templates/support-info.php:12
669
+ msgid "Read our FAQs"
670
+ msgstr ""
671
+
672
+ #: includes/templates/support-info.php:13
673
+ msgid "Getting started made easy – read the FAQs to jumpstart your business."
674
+ msgstr ""
675
+
676
+ #: includes/templates/support-info.php:13
677
+ msgid ""
678
+ "Whether you're a video tutorial fan or prefer the written answers – we've "
679
+ "got it covered!"
680
+ msgstr ""
681
+
682
+ #: includes/templates/support-info.php:15
683
+ msgid "See Printful FAQ"
684
+ msgstr ""
685
+
686
+ #: includes/templates/support-info.php:20
687
+ #: includes/templates/support-info.php:23
688
+ msgid "Integration help"
689
+ msgstr ""
690
+
691
+ #: includes/templates/support-info.php:21
692
+ msgid ""
693
+ "Are you experiencing technical issues? Solve them on your own by reading "
694
+ "these helpful guides and video tutorials."
695
+ msgstr ""
trunk/includes/class-printful-admin-dashboard.php ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Admin_Dashboard {
5
+
6
+ const API_KEY_SEARCH_STRING = 'Printful';
7
+
8
+ public static $_instance;
9
+
10
+ /**
11
+ * @return Printful_Admin_Dashboard
12
+ */
13
+ public static function instance() {
14
+
15
+ if ( is_null( self::$_instance ) ) {
16
+ self::$_instance = new self();
17
+ }
18
+
19
+ return self::$_instance;
20
+ }
21
+
22
+ /**
23
+ * Printful_Admin_Dashboard constructor.
24
+ */
25
+ function __construct() {
26
+
27
+ }
28
+
29
+ /**
30
+ * Show the view
31
+ * @throws PrintfulException
32
+ */
33
+ public static function view() {
34
+
35
+ $dashboard = self::instance();
36
+ $api_key = Printful_Integration::instance()->get_option( 'printful_key' );
37
+ $connect_status = Printful_Integration::instance()->is_connected();
38
+
39
+ if ( $connect_status ) {
40
+ $dashboard->render_dashboard();
41
+ } else if(!$connect_status && strlen($api_key) > 0) {
42
+ $dashboard->render_connect_error();
43
+ } else {
44
+ $dashboard->render_connect();
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Display the Printful connect page
50
+ */
51
+ public function render_connect() {
52
+
53
+ $status = Printful_Admin_Status::instance();
54
+ $issues = array();
55
+
56
+ $permalinks_set = $status->run_single_test( 'check_permalinks' );
57
+
58
+ if ( $permalinks_set == Printful_Admin_Status::PF_STATUS_FAIL ) {
59
+ $message = 'WooCommerce API will not work unless your permalinks are set up correctly. Go to <a href="%s">Permalinks settings</a> and make sure that they are NOT set to "plain".';
60
+ $settings_url = admin_url( 'options-permalink.php' );
61
+ $issues[] = sprintf( $message, $settings_url );
62
+ }
63
+
64
+ if ( strpos( get_site_url(), 'localhost' ) ) {
65
+ $issues[] = 'You can\'t connect to Printful from localhost. Printful needs to be able reach your site to establish a connection.';
66
+ }
67
+
68
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
69
+
70
+ Printful_Admin::load_template( 'connect', array(
71
+ 'consumer_key' => $this->_get_consumer_key(),
72
+ 'waiting_sync' => isset( $_GET['sync-in-progress'] ),
73
+ 'consumer_key_error' => isset( $_GET['consumer-key-error'] ),
74
+ 'issues' => $issues,
75
+ )
76
+ );
77
+
78
+ if ( isset( $_GET['sync-in-progress'] ) ) {
79
+ $emit_auth_response = 'Printful_Connect.send_return_message();';
80
+ Printful_Admin::load_template( 'inline-script', array( 'script' => $emit_auth_response ) );
81
+ }
82
+
83
+ Printful_Admin::load_template('footer');
84
+ }
85
+
86
+ /**
87
+ * Display the Printful connect error page
88
+ */
89
+ public function render_connect_error() {
90
+
91
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
92
+
93
+ $connect_error = Printful_Integration::instance()->get_connect_error();
94
+ if ( $connect_error ) {
95
+ Printful_Admin::load_template('error', array('error' => $connect_error));
96
+ }
97
+
98
+ Printful_Admin::load_template('footer');
99
+ }
100
+
101
+ /**
102
+ * Display the dashboard
103
+ */
104
+ public function render_dashboard() {
105
+
106
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
107
+
108
+ $stats = $this->_get_stats(true);
109
+ $orders = $this->_get_orders(true);
110
+ $error = false;
111
+
112
+ if ( is_wp_error( $stats ) ) {
113
+ $error = $stats;
114
+ }
115
+ if ( is_wp_error( $orders ) ) {
116
+ $error = $orders;
117
+ }
118
+
119
+ if ( ! $error ) {
120
+
121
+ if ( $stats ) {
122
+ Printful_Admin::load_template( 'stats', array( 'stats' => $stats ) );
123
+ } else {
124
+ Printful_Admin::load_template( 'ajax-loader', array( 'action' => 'get_printful_stats', 'message' => __( 'Loading your stats...', 'printful' ) ) );
125
+ }
126
+
127
+ if ($orders) {
128
+ Printful_Admin::load_template( 'order-table', array( 'orders' => $orders ) );
129
+ } else {
130
+ Printful_Admin::load_template( 'ajax-loader', array( 'action' => 'get_printful_orders', 'message' => __( 'Loading your orders...', 'printful' ) ) );
131
+ }
132
+
133
+ } else {
134
+ Printful_Admin::load_template( 'error', array( 'error' => $error->get_error_message('printful') ) );
135
+ }
136
+
137
+ Printful_Admin::load_template( 'quick-links' );
138
+
139
+ if ( isset( $_GET['sync-in-progress'] ) ) {
140
+ $emit_auth_response = 'Printful_Connect.send_return_message();';
141
+ Printful_Admin::load_template( 'inline-script', array( 'script' => $emit_auth_response ) );
142
+ }
143
+
144
+ Printful_Admin::load_template( 'footer' );
145
+ }
146
+
147
+ /**
148
+ * Ajax response for stats block
149
+ */
150
+ public static function render_stats_ajax() {
151
+
152
+ $stats = self::instance()->_get_stats();
153
+
154
+ if ( ! empty( $stats ) && ! is_wp_error( $stats ) ) {
155
+ Printful_Admin::load_template( 'stats', array( 'stats' => $stats ) );
156
+ } else {
157
+ Printful_Admin::load_template( 'error', array( 'error' => $stats->get_error_message( 'printful' ) ) );
158
+ }
159
+
160
+ exit;
161
+ }
162
+
163
+ /**
164
+ * Ajax response for stats block
165
+ */
166
+ public static function render_orders_ajax() {
167
+
168
+ $orders = self::instance()->_get_orders();
169
+
170
+ if ( ! empty( $orders ) && is_wp_error( $orders ) ) {
171
+ Printful_Admin::load_template( 'error', array( 'error' => $orders->get_error_message('printful') ) );
172
+ } else {
173
+ Printful_Admin::load_template( 'order-table', array( 'orders' => $orders ) );
174
+ }
175
+
176
+ exit;
177
+ }
178
+
179
+ /**
180
+ * Get store statistics from API
181
+ * @param bool $only_cached_results
182
+ * @return mixed
183
+ */
184
+ private function _get_stats($only_cached_results = false) {
185
+
186
+ $stats = get_transient( 'printful_stats' );
187
+ if ( $only_cached_results || $stats ) {
188
+ return $stats;
189
+ }
190
+
191
+ try {
192
+ $stats = Printful_Integration::instance()->get_client()->get( 'store/statistics' );
193
+ if ( ! empty( $stats['store_statistics'] ) ) {
194
+ $stats = $stats['store_statistics'];
195
+ }
196
+ set_transient( 'printful_stats', $stats, MINUTE_IN_SECONDS * 5 ); //cache for 5 minute
197
+ } catch (PrintfulApiException $e) {
198
+ return new WP_Error('printful', 'Could not connect to Printful API. Please try again later!');
199
+ } catch (PrintfulException $e) {
200
+ return new WP_Error('printful', 'Could not connect to Printful API. Please try again later!');
201
+ }
202
+
203
+ return $stats;
204
+ }
205
+
206
+ /**
207
+ * Get Printful orders from the API
208
+ * @param bool $only_cached_results
209
+ * @return mixed
210
+ */
211
+ private function _get_orders($only_cached_results = false) {
212
+
213
+ $orders = get_transient( 'printful_orders' );
214
+
215
+ if ( $only_cached_results || $orders ) {
216
+ return $orders;
217
+ }
218
+
219
+ try {
220
+ $order_data = Printful_Integration::instance()->get_client()->get( 'orders' );
221
+
222
+ if ( ! empty( $order_data ) ) {
223
+
224
+ foreach ( $order_data as $key => $order ) {
225
+
226
+ if($order['status'] == 'pending') {
227
+ $order_data[$key]['status'] = 'Waiting for fulfillment';
228
+ }
229
+ }
230
+ }
231
+
232
+ $orders = array( 'count' => count( $order_data ), 'results' => $order_data );
233
+ set_transient( 'printful_orders', $orders, MINUTE_IN_SECONDS * 5 ); //cache for 5 minute
234
+ } catch (PrintfulApiException $e) {
235
+ return new WP_Error('printful', 'Could not connect to Printful API. Please try again later!');
236
+ } catch (PrintfulException $e) {
237
+ return new WP_Error('printful', 'Could not connect to Printful API. Please try again later!');
238
+ }
239
+
240
+ return $orders;
241
+ }
242
+
243
+ /**
244
+ * Get the last used consumer key fragment and use it for validating the address
245
+ * @return null|string
246
+ */
247
+ private function _get_consumer_key() {
248
+
249
+ global $wpdb;
250
+
251
+ // Get the API key
252
+ $printfulKey = '%' . esc_sql( $wpdb->esc_like( wc_clean( self::API_KEY_SEARCH_STRING ) ) ) . '%';
253
+ $consumer_key = $wpdb->get_var(
254
+ $wpdb->prepare(
255
+ "SELECT truncated_key FROM {$wpdb->prefix}woocommerce_api_keys WHERE description LIKE %s ORDER BY key_id DESC LIMIT 1",
256
+ $printfulKey
257
+ )
258
+ );
259
+
260
+ //if not found by description, it was probably manually created. try the last used key instead
261
+ if ( ! $consumer_key ) {
262
+ $consumer_key = $wpdb->get_var(
263
+ "SELECT truncated_key FROM {$wpdb->prefix}woocommerce_api_keys ORDER BY key_id DESC LIMIT 1"
264
+ );
265
+ }
266
+
267
+ return $consumer_key;
268
+ }
269
+
270
+ }
trunk/includes/class-printful-admin-settings.php ADDED
@@ -0,0 +1,466 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Admin_Settings {
5
+
6
+ public static $_instance;
7
+ const CARRIER_TYPE_STANDARD = 'standard';
8
+ const CARRIER_TYPE_EXPEDITED = 'expedited';
9
+ const CARRIER_TYPE_DOMESTIC = 'domestic';
10
+ const CARRIER_TYPE_INTERNATIONAL = 'international';
11
+ const CARRIER_REGION_US = 'US';
12
+ const CARRIER_REGION_EU = 'LV';
13
+ const DEFAULT_PERSONALIZE_BUTTON_TEXT = 'Personalize Design';
14
+ const DEFAULT_PERSONALIZE_BUTTON_COLOR = '#eee';
15
+ const DEFAULT_PERSONALIZE_MODAL_TITLE = 'Create a personalized design';
16
+
17
+ // Size guide modal settings
18
+ const DEFAULT_SIZE_GUIDE_BUTTON_TEXT = 'Size Guide';
19
+ const DEFAULT_SIZE_GUIDE_BUTTON_COLOR = '#1164A9';
20
+ const DEFAULT_SIZE_GUIDE_MODAL_TITLE = 'Size guide';
21
+ const DEFAULT_SIZE_GUIDE_MODAL_TEXT_COLOR = '#000';
22
+ const DEFAULT_SIZE_GUIDE_MODAL_BACKGROUND_COLOR = '#fff';
23
+ const DEFAULT_SIZE_GUIDE_TAB_BACKGROUND_COLOR = '#fff';
24
+ const DEFAULT_SIZE_GUIDE_ACTIVE_TAB_BACKGROUND_COLOR = '#fff';
25
+ const DEFAULT_SIZE_GUIDE_UNIT = 'inch';
26
+
27
+ /**
28
+ * @return array
29
+ */
30
+ public static function getIntegrationFields()
31
+ {
32
+ $sales_tax_link = sprintf(
33
+ '<a href="%s" target="_blank">%s</a>',
34
+ esc_url( 'https://www.printful.com/faq/taxes-and-billing/sales-tax/371-which-states-does-printful-charge-sales-tax-in-' ),
35
+ esc_html__( 'states where Printful applies sales tax', 'printful' )
36
+ );
37
+
38
+ return array(
39
+ 'printful_key' => array(
40
+ 'title' => __( 'Printful store API key', 'printful' ),
41
+ 'type' => 'text',
42
+ 'desc_tip' => true,
43
+ 'description' => __( 'Your store\'s Printful API key. Create it in the Prinful dashboard', 'printful' ),
44
+ 'default' => false,
45
+ ),
46
+ 'calculate_tax' => array(
47
+ 'title' => __( 'Calculate sales tax', 'printful' ),
48
+ 'type' => 'checkbox',
49
+ 'label' => sprintf(
50
+ __('Calculated for all products listed on your store (including non-Printful products) that ship to %s. Before enabling, make sure you are registered for a seller permit in all these states.'),
51
+ $sales_tax_link
52
+ ),
53
+ 'default' => 'no',
54
+ ),
55
+ 'disable_ssl' => array(
56
+ 'title' => __( 'Disable SSL', 'printful' ),
57
+ 'type' => 'checkbox',
58
+ 'label' => __( 'Use HTTP instead of HTTPS to connect to the Printful API (may be required if the plugin does not work for some hosting configurations)', 'printful' ),
59
+ 'default' => 'no',
60
+ ),
61
+ );
62
+ }
63
+
64
+ public static function getPersonalizationFields()
65
+ {
66
+ return array(
67
+ 'pfc_button_text' => array(
68
+ 'title' => __( 'Personalization button text', 'printful' ),
69
+ 'type' => 'text',
70
+ 'description' => __( 'Personalization button text', 'printful' ),
71
+ 'default' => self::DEFAULT_PERSONALIZE_BUTTON_TEXT
72
+ ),
73
+ 'pfc_button_color' => array(
74
+ 'title' => __( 'Personalization button color', 'printful' ),
75
+ 'type' => 'color-picker',
76
+ 'description' => __( 'Personalization button background color', 'printful' ),
77
+ 'default' => self::DEFAULT_PERSONALIZE_BUTTON_COLOR,
78
+ ),
79
+ 'pfc_modal_title' => array(
80
+ 'title' => __( 'Personalization popup title', 'printful' ),
81
+ 'type' => 'text',
82
+ 'description' => __( 'Personalization popup title text', 'printful' ),
83
+ 'default' => self::DEFAULT_PERSONALIZE_MODAL_TITLE,
84
+ ),
85
+ );
86
+ }
87
+
88
+ public static function getSizeGuideFields()
89
+ {
90
+ return array(
91
+ 'pfsg_modal_title' => array(
92
+ 'title' => __( 'Size guide popup title', 'printful' ),
93
+ 'type' => 'text',
94
+ 'description' => __( 'Size guide popup title text', 'printful' ),
95
+ 'default' => self::DEFAULT_SIZE_GUIDE_MODAL_TITLE,
96
+ ),
97
+ 'pfsg_modal_text_color' => array(
98
+ 'title' => __( 'Size guide popup text color', 'printful' ),
99
+ 'type' => 'color-picker',
100
+ 'description' => __( 'Size guide popup text color', 'printful' ),
101
+ 'default' => self::DEFAULT_SIZE_GUIDE_MODAL_TEXT_COLOR,
102
+ ),
103
+ 'pfsg_modal_background_color' => array(
104
+ 'title' => __( 'Size guide popup background color', 'printful' ),
105
+ 'type' => 'color-picker',
106
+ 'description' => __( 'Size guide popup background color', 'printful' ),
107
+ 'default' => self::DEFAULT_SIZE_GUIDE_MODAL_BACKGROUND_COLOR,
108
+ ),
109
+ 'pfsg_tab_background_color' => array(
110
+ 'title' => __( 'Size guide tab background color', 'printful' ),
111
+ 'type' => 'color-picker',
112
+ 'description' => __( 'Size guide tab background color', 'printful' ),
113
+ 'default' => self::DEFAULT_SIZE_GUIDE_TAB_BACKGROUND_COLOR,
114
+ ),
115
+ 'pfsg_active_tab_background_color' => array(
116
+ 'title' => __( 'Size guide active tab background color', 'printful' ),
117
+ 'type' => 'color-picker',
118
+ 'description' => __( 'Size guide active tab background color', 'printful' ),
119
+ 'default' => self::DEFAULT_SIZE_GUIDE_ACTIVE_TAB_BACKGROUND_COLOR,
120
+ ),
121
+ 'pfsg_button_text' => array(
122
+ 'title' => __( 'Size guide button text', 'printful' ),
123
+ 'type' => 'text',
124
+ 'description' => __( 'Size guide button text', 'printful' ),
125
+ 'default' => self::DEFAULT_SIZE_GUIDE_BUTTON_TEXT,
126
+ ),
127
+ 'pfsg_button_color' => array(
128
+ 'title' => __( 'Size guide button text color', 'printful' ),
129
+ 'type' => 'color-picker',
130
+ 'description' => __( 'Size guide button text color', 'printful' ),
131
+ 'default' => self::DEFAULT_SIZE_GUIDE_BUTTON_COLOR,
132
+ ),
133
+ 'pfsg_primary_unit' => array(
134
+ 'title' => __( 'Primary measurement unit', 'printful' ),
135
+ 'type' => 'dropdown',
136
+ 'description' => __( 'Primary measurement unit (cm/inch)', 'printful' ),
137
+ 'default' => self::DEFAULT_SIZE_GUIDE_UNIT,
138
+ 'selected' => Printful_Integration::instance()->get_option( 'pfsg_primary_unit' ),
139
+ 'items' => [
140
+ 'inch' => __('Inches', 'printful'),
141
+ 'centimeter' => __('Centimeters', 'printful'),
142
+ ],
143
+ ),
144
+ );
145
+ }
146
+
147
+ /**
148
+ * @return array
149
+ */
150
+ public static function getAllFields() {
151
+ return array_merge(self::getIntegrationFields(), self::getPersonalizationFields(), self::getSizeGuideFields());
152
+ }
153
+
154
+ /**
155
+ * @return Printful_Admin_Settings
156
+ */
157
+ public static function instance() {
158
+
159
+ if ( is_null( self::$_instance ) ) {
160
+ self::$_instance = new self();
161
+ }
162
+
163
+ return self::$_instance;
164
+ }
165
+
166
+ /**
167
+ * Setup the view
168
+ */
169
+ public static function view() {
170
+
171
+ $settings = self::instance();
172
+ $settings->render();
173
+ }
174
+
175
+ /**
176
+ * Display the view
177
+ */
178
+ public function render() {
179
+
180
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
181
+
182
+ echo '<form method="post" name="printful_settings" action="' . esc_url( admin_url( 'admin-ajax.php?action=save_printful_settings' ) ) . '">';
183
+
184
+ // Integration settings
185
+ $integration_settings = $this->setup_fields( __('Integration settings', 'printful'), '', self::getIntegrationFields() );
186
+ Printful_Admin::load_template( 'setting-group', $integration_settings );
187
+
188
+ // Product personalization settings
189
+ $personalization_settings = $this->setup_fields( __('Product personalization settings', 'printful'), '', self::getPersonalizationFields() );
190
+ Printful_Admin::load_template( 'setting-group', $personalization_settings );
191
+
192
+ // Size guide settings
193
+ $size_guide_settings = $this->setup_fields(
194
+ __('Size guide settings', 'printful'),
195
+ __('These settings control how the new size guide will look on your WooCommerce storefront. Products with an old size guide will not be affected.', 'printful'),
196
+ self::getSizeGuideFields()
197
+ );
198
+ Printful_Admin::load_template( 'setting-group', $size_guide_settings );
199
+
200
+ Printful_Admin::load_template( 'shipping-notification' );
201
+
202
+ //carriers settings
203
+ Printful_Admin::load_template( 'ajax-loader', array( 'action' => 'get_printful_carriers', 'message' => 'Loading your carriers...' ) );
204
+
205
+ Printful_Admin::load_template( 'setting-submit', array( 'nonce' => wp_create_nonce( 'printful_settings' ), 'disabled' => true ) );
206
+
207
+ echo '</form>';
208
+
209
+ Printful_Admin::load_template( 'footer' );
210
+ }
211
+
212
+ /**
213
+ * Display the ajax content for carrier settings
214
+ * @throws PrintfulException
215
+ */
216
+ public static function render_carriers_ajax() {
217
+
218
+ $carrier_settings = self::instance()->setup_carrier_fields();
219
+ Printful_Admin::load_template( 'setting-group', $carrier_settings );
220
+ $enable_submit = 'Printful_Settings.enable_submit_btn();';
221
+ Printful_Admin::load_template( 'inline-script', array( 'script' => $enable_submit ) );
222
+
223
+ exit;
224
+ }
225
+
226
+ /**
227
+ * @param string $title Settings section title
228
+ * @param string $description Section description
229
+ * @param array $fields
230
+ *
231
+ * @return array
232
+ */
233
+ public function setup_fields($title, $description = '', $fields = [])
234
+ {
235
+ $fieldGroup = array(
236
+ 'title' => $title,
237
+ 'description' => $description,
238
+ 'settings' => $fields,
239
+ );
240
+
241
+ foreach ( $fieldGroup['settings'] as $key => $setting ) {
242
+ if ( $setting['type'] !== 'title' ) {
243
+ $fieldGroup['settings'][ $key ]['value'] = Printful_Integration::instance()->get_option( $key, $setting['default'] );
244
+ }
245
+ }
246
+
247
+ return $fieldGroup;
248
+ }
249
+
250
+ /**
251
+ * @internal param $carrier_settings
252
+ * @throws PrintfulException
253
+ */
254
+ public function setup_carrier_fields() {
255
+ $carrier_settings = array(
256
+ 'title' => __( 'Shipping Methods', 'printful' ),
257
+ 'description' => __( 'Here you can choose the shipping methods you want Printful to use for shipping your orders.
258
+ Uncheck the ones you want to disable. From your selection, our algorithm will determine the fastest, most cost-effective, and most reliable method for each order.', 'printful' ),
259
+ 'settings' => array(),
260
+ );
261
+
262
+ if ( ! Printful_Integration::instance()->is_connected() ) {
263
+ $carrier_settings['description'] = __( 'You need to be connected to Printful API to edit carrier settings!', 'printful' );
264
+
265
+ return $carrier_settings;
266
+ }
267
+
268
+ $carrier_regions = Printful_Carriers::instance()->carriers;
269
+
270
+ if ( empty( $carrier_regions ) ) {
271
+ return false;
272
+ }
273
+
274
+ $carrier_settings[ 'settings' ] = $this->prepare_form_data( $carrier_regions );
275
+
276
+ return $carrier_settings;
277
+ }
278
+
279
+ /**
280
+ * Prepare carrier data for posting to Printful API
281
+ * @return array|bool
282
+ */
283
+ public function prepare_carriers() {
284
+
285
+ $carrier_regions = Printful_Carriers::instance()->carriers;
286
+
287
+ if ( empty( $carrier_regions ) ) {
288
+ return false;
289
+ }
290
+
291
+ $us_carriers[ self::CARRIER_REGION_US ] = ( ( ! empty( $_POST[ self::CARRIER_REGION_US ] ) && wp_verify_nonce( $_POST['_wpnonce'], 'printful_settings' ) ) ? $_POST[ self::CARRIER_REGION_US ] : array() );
292
+ $eu_carriers[ self::CARRIER_REGION_EU ] = ( ( ! empty( $_POST[ self::CARRIER_REGION_EU ] ) && wp_verify_nonce( $_POST['_wpnonce'], 'printful_settings' ) ) ? $_POST[ self::CARRIER_REGION_EU ] : array() );
293
+
294
+ $parsed_carriers = $this->parse_region_carriers( $us_carriers, $eu_carriers );
295
+ $us_carriers = $parsed_carriers['us_carriers'];
296
+ $eu_carriers = $parsed_carriers['eu_carriers'];
297
+
298
+ $saved_carriers = array_merge( $us_carriers, $eu_carriers );
299
+ $request_body = array();
300
+
301
+ foreach ( $carrier_regions as $region => $carrier_region ) {
302
+ foreach ( $carrier_region as $carrier_type => $carrier_methods ) {
303
+ foreach ( $carrier_methods as $carrier_method => $carrier_data ) {
304
+ $is_active = false;
305
+
306
+ if ( in_array( $carrier_method, $saved_carriers[ $region ][ $carrier_type ] ) ) {
307
+ $is_active = true;
308
+ }
309
+
310
+ // `isDisabled` means that user is not allowed to change `isActive` state
311
+ if ($carrier_data['isDisabled']) {
312
+ $is_active = $carrier_data['isActive'];
313
+ }
314
+
315
+ $request_body[ $region ][ $carrier_type ][ $carrier_method ] = array(
316
+ 'isActive' => $is_active
317
+ );
318
+ }
319
+ }
320
+ }
321
+
322
+ return $request_body;
323
+ }
324
+
325
+ /**
326
+ * Ajax endpoint for saving the settings
327
+ * @throws PrintfulException
328
+ */
329
+ public static function save_printful_settings() {
330
+
331
+ if ( ! empty( $_POST ) ) {
332
+
333
+ check_admin_referer( 'printful_settings' );
334
+ $error_msg = null;
335
+
336
+ //save carriers first, so API key change does not affect this
337
+ if ( Printful_Integration::instance()->is_connected(true) ) {
338
+
339
+ //save remote carrier settings
340
+ $request_body = Printful_Admin_Settings::instance()->prepare_carriers();
341
+ $result = Printful_Carriers::instance()->post_carriers( $request_body );
342
+
343
+ if ( ! $result ) {
344
+ $error_msg = 'Error: failed to save carriers';
345
+ } else if (isset($result['error'])) {
346
+ $error_msg = $result['error'];
347
+ }
348
+ }
349
+
350
+ $options = array();
351
+
352
+ //build save options list
353
+ foreach ( self::getAllFields() as $key => $field ) {
354
+
355
+ if ( $field['type'] == 'checkbox' ) {
356
+ if ( isset( $_POST[ $key ] ) ) {
357
+ $options[ $key ] = 'yes';
358
+ } else {
359
+ $options[ $key ] = 'no';
360
+ }
361
+ } else {
362
+ if ( isset( $_POST[ $key ] ) ) {
363
+ $options[ $key ] = $_POST[ $key ];
364
+ }
365
+ }
366
+ }
367
+
368
+ //save integration settings
369
+ Printful_Integration::instance()->update_settings( $options );
370
+
371
+ if ( $error_msg ) {
372
+ die( $error_msg );
373
+ }
374
+
375
+ die('OK');
376
+ }
377
+ }
378
+
379
+ /**
380
+ * @param array $carrier_regions
381
+ * @return array|bool
382
+ */
383
+ private function prepare_form_data( $carrier_regions )
384
+ {
385
+ $carrier_settings = array();
386
+
387
+ foreach ( $carrier_regions as $region => $carrier_types ) {
388
+ foreach ($carrier_types as $carrier_type => $carrier_data) {
389
+ foreach ($carrier_data as $key => $carrier) {
390
+ $carrier_item = array(
391
+ 'label' => $carrier[ 'label' ] . ' <i>' . $carrier[ 'subtitle' ] . '</i>',
392
+ 'value' => ( $carrier[ 'isActive' ] == true ? 'yes' : 'no' ),
393
+ 'isDisabled' => $carrier[ 'isDisabled' ]
394
+ );
395
+
396
+ $carrier_regions[ $region ][ $carrier_type ][ $carrier['key'] ] = $carrier_item;
397
+ }
398
+ }
399
+ }
400
+
401
+ $item_array = $this->format_carrier_items( $carrier_regions );
402
+
403
+ $carrier_settings[ self::CARRIER_REGION_US ] = array(
404
+ 'title' => __( 'International from USA', 'printful' ),
405
+ 'type' => 'checkbox-group',
406
+ 'items' => $item_array[ self::CARRIER_REGION_US ]
407
+ );
408
+
409
+ $carrier_settings[ self::CARRIER_REGION_EU ] = array(
410
+ 'title' => __( 'International from EU', 'printful' ),
411
+ 'type' => 'checkbox-group',
412
+ 'items' => $item_array[ self::CARRIER_REGION_EU ]
413
+ );
414
+
415
+ return $carrier_settings;
416
+ }
417
+
418
+ /**
419
+ * Prepare carrier item data for form
420
+ * @param array $carrier_regions
421
+ * @return array
422
+ */
423
+ private function format_carrier_items( $carrier_regions )
424
+ {
425
+ $item_array = array();
426
+
427
+ foreach ( $carrier_regions as $region => $carrier_types ) {
428
+ foreach ( $carrier_types as $carrier_type => $carrier_data ) {
429
+ $item_array[ $region ][ $carrier_type ] = array(
430
+ 'subtitle' => ucfirst($carrier_type) . ' shipping',
431
+ 'carriers' => $carrier_data
432
+ );
433
+ }
434
+ }
435
+
436
+ return array(
437
+ self::CARRIER_REGION_US => $item_array[ self::CARRIER_REGION_US ],
438
+ self::CARRIER_REGION_EU => $item_array[ self::CARRIER_REGION_EU ]
439
+ );
440
+ }
441
+
442
+ /**
443
+ * @param array $us_carriers
444
+ * @param array $eu_carriers
445
+ * @return array
446
+ */
447
+ private function parse_region_carriers( $us_carriers, $eu_carriers )
448
+ {
449
+ foreach ( $us_carriers[ self::CARRIER_REGION_US ] as $carrier_type => $carrier_methods ) {
450
+ foreach ( $carrier_methods as $key => $carrier_method ) {
451
+ $us_carriers[ self::CARRIER_REGION_US ][ $carrier_type ][ $key ] = str_replace( self::CARRIER_REGION_US . '_' . $carrier_type . '_', '', $carrier_method );
452
+ }
453
+ }
454
+
455
+ foreach ( $eu_carriers[ self::CARRIER_REGION_EU ] as $carrier_type => $carrier_methods ) {
456
+ foreach ( $carrier_methods as $key => $carrier_method ) {
457
+ $eu_carriers[ self::CARRIER_REGION_EU ][ $carrier_type ][ $key ] = str_replace( self::CARRIER_REGION_EU . '_' . $carrier_type.'_', '', $carrier_method );
458
+ }
459
+ }
460
+
461
+ return array(
462
+ 'eu_carriers' => $eu_carriers,
463
+ 'us_carriers' => $us_carriers
464
+ );
465
+ }
466
+ }
trunk/includes/class-printful-admin-status.php ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Admin_Status {
5
+
6
+ const PF_STATUS_OK = 1;
7
+ const PF_STATUS_WARNING = 0;
8
+ const PF_STATUS_FAIL = -1;
9
+ const PF_STATUS_NOT_CONNECTED = 2;
10
+
11
+ const API_KEY_SEARCH_STRING = 'Printful';
12
+ const PF_WEBHOOK_NAME = 'Printful Integration';
13
+ const PF_REMOTE_REQUEST_URL = 'hook/woocommerce?store=1';
14
+ const PF_REMOTE_REQUEST_TOPIC = 'woo.plugin.test';
15
+ const PF_STATUS_ISSUE_COUNT = 'printful_status_issue_count';
16
+ const PF_CACHED_CHECKLIST = 'printful_cached_checklist';
17
+ const STATUS_CONNECTED_DISPLAYABLE_METHODS = array(
18
+ 'check_PF_webhooks',
19
+ 'check_PF_sync_errors',
20
+ 'check_PF_API_key',
21
+ 'check_WC_API_access'
22
+ );
23
+
24
+ public static $_instance;
25
+
26
+ public static function getChecklistItems() {
27
+ return array(
28
+ array(
29
+ 'name' => __( 'Connection to Printful API', 'printful' ),
30
+ 'description' => __( 'Is your store successfully connected to Printful API?', 'printful' ),
31
+ 'method' => 'check_PF_API_connect',
32
+ ),
33
+ array(
34
+ 'name' => __( 'Printful API key is set', 'printful' ),
35
+ 'description' => __( 'Your store needs access to Printful API to use most of it\'s features like shipping rates, tax rates and other settings.', 'printful' ),
36
+ 'method' => 'check_PF_API_key',
37
+ ),
38
+ array(
39
+ 'name' => __( 'WordPress Permalinks', 'printful' ),
40
+ 'description' => __( 'WooCommerce API will not work unless your permalinks in Settings > Permalinks are set up correctly. Make sure you that they are NOT set to "plain".', 'printful' ),
41
+ 'method' => 'check_permalinks',
42
+ ),
43
+ array(
44
+ 'name' => __( 'WordPress version', 'printful' ),
45
+ 'description' => __( 'WordPress should always be updated to the latest version. Updates can be installed from your WordPress admin dashboard.', 'printful' ),
46
+ 'method' => 'check_WP_version',
47
+ ),
48
+ array(
49
+ 'name' => __( 'WooCommerce Webhooks', 'printful' ),
50
+ 'description' => __( 'Printful requires WooCommerce webhooks to be set up to quickly capture you incoming orders, products updates etc.', 'printful' ),
51
+ 'method' => 'check_PF_webhooks',
52
+ ),
53
+ array(
54
+ 'name' => __( 'WooCommerce API keys are set', 'printful' ),
55
+ 'description' => __( 'Printful needs access to your WooCommerce API for the integration to work - otherwise we can\'t sync your store, push or pull your products etc.', 'printful' ),
56
+ 'method' => 'check_WC_API_access',
57
+ ),
58
+ array(
59
+ 'name' => __( 'WooCommerce authentication URL access', 'printful' ),
60
+ 'description' => __( 'Printful needs access to WooCommerce API authorize page. This sometimes may get blocked due to hosts having unnecessarily intrusive security checks in place that prevent WooCommerce API authentication from working (for example mod_security rule #1234234). If this check fails, you will not be able authorize Printful app.', 'printful' ),
61
+ 'method' => 'check_WC_auth_url_access',
62
+ ),
63
+ array(
64
+ 'name' => __( 'WordPress remote requests', 'printful' ),
65
+ 'description' => __( 'WordPress needs to be able to connect to Printful server to call webhooks. If this check fails, contact your hosting support.', 'printful' ),
66
+ 'method' => 'check_remote_requests',
67
+ ),
68
+ array(
69
+ 'name' => __( 'WordPress Site URL', 'printful' ),
70
+ 'description' => __( 'If your currently setup WordPress site URL is redirected to another URL the integration might not work correctly. Typically this happens with incorrect http to https redirects. Go to Settings > General to fix this.' , 'printful' ),
71
+ 'method' => 'check_site_url_redirect',
72
+ ),
73
+ array(
74
+ 'name' => __( 'Recent store sync errors', 'printful' ),
75
+ 'description' => __( 'Printful will connect to your store\'s API regularly and sync your latest products, orders etc. If there have been any recent issues with sync, this check will fail.', 'printful' ),
76
+ 'method' => 'check_PF_sync_errors',
77
+ ),
78
+ array(
79
+ 'name' => __( 'Write permissions', 'printful' ),
80
+ 'description' => __( 'Make the uploads directory writable. This is required for mockup generator product push to work correctly. Contact your hosting provider if you need help with this.', 'printful' ),
81
+ 'method' => 'check_uploads_write',
82
+ ),
83
+ array(
84
+ 'name' => __( 'PHP memory limit', 'printful' ),
85
+ 'description' => __( 'Set PHP allocated memory limit to at least 128mb. Contact your hosting provider if you need help with this.', 'printful' ),
86
+ 'method' => 'check_PHP_memory_limit',
87
+ ),
88
+ array(
89
+ 'name' => __( 'PHP script time limit', 'printful' ),
90
+ 'description' => __( 'Set PHP script execution time limit to at least 30 seconds. This is required to successfully push products with many variants. Contact your hosting provider if you need help with this.', 'printful' ),
91
+ 'method' => 'check_PHP_time_limit',
92
+ ),
93
+ array(
94
+ 'name' => __( 'W3 Total Cache DB Cache', 'printful' ),
95
+ 'description' => __( 'If you are using W3 Total Cache, the database caching feature needs to be disabled since it can cause issues with product push to store.', 'printful' ),
96
+ 'method' => 'check_W3_db_cache',
97
+ 'silent' => true,
98
+ ),
99
+ array(
100
+ 'name' => __( 'WP SpamShield', 'printful' ),
101
+ 'description' => __( 'If you are using WP SpamShield, you might experience problems connecting to Printful and pushing products.', 'printful' ),
102
+ 'method' => 'check_wp_spamshield',
103
+ 'silent' => true,
104
+ ),
105
+ array(
106
+ 'name' => __( 'Remove Print Aura plugin', 'printful' ),
107
+ 'description' => __( 'Print Aura plugin is known to cause issues so it needs to be removed.', 'printful' ),
108
+ 'method' => 'check_printaura_plugin',
109
+ 'silent' => true,
110
+ ),
111
+ );
112
+ }
113
+
114
+ /**
115
+ * @return Printful_Admin_Status
116
+ */
117
+ public static function instance() {
118
+ if ( ! function_exists( 'get_plugins' ) ) {
119
+ include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
120
+ }
121
+
122
+ if ( is_null( self::$_instance ) ) {
123
+ self::$_instance = new self();
124
+ }
125
+
126
+ return self::$_instance;
127
+ }
128
+
129
+ /**
130
+ * Setup the view variables
131
+ */
132
+ public static function view() {
133
+
134
+ $status = self::instance();
135
+ $status->render();
136
+ }
137
+
138
+ /**
139
+ * Render the view
140
+ */
141
+ public function render() {
142
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
143
+
144
+ $checklist = self::get_checklist( true );
145
+ if ( $checklist ) {
146
+ Printful_Admin::load_template( 'status-table', array( 'checklist' => $checklist ) );
147
+ } else {
148
+ Printful_Admin::load_template( 'ajax-loader', array( 'action' => 'get_printful_status_checklist', 'message' => __( 'Testing your store (this may take up to 30 seconds)...', 'printful' ) ) );
149
+ }
150
+
151
+ Printful_Admin::load_template( 'footer' );
152
+ }
153
+
154
+ /**
155
+ * Build the content for status page
156
+ */
157
+ public static function render_status_table_ajax() {
158
+
159
+ $checklist = self::get_checklist();
160
+ Printful_Admin::load_template( 'status-table', array( 'checklist' => $checklist ) );
161
+
162
+ exit;
163
+ }
164
+
165
+ /**
166
+ * Run the tests
167
+ * @param bool $only_cached_results
168
+ * @return array
169
+ * @throws PrintfulException
170
+ */
171
+ public static function get_checklist( $only_cached_results = false ) {
172
+
173
+ $status = self::instance();
174
+
175
+ $list = get_transient( Printful_Admin_Status::PF_CACHED_CHECKLIST );
176
+
177
+ if ( $only_cached_results || $list ) {
178
+ return $list;
179
+ }
180
+
181
+ $list = array();
182
+ $list['overall_status'] = true;
183
+ $issueCount = 0;
184
+
185
+ foreach ( self::getChecklistItems() as $item ) {
186
+ $list_item = array();
187
+ $list_item['name'] = $item['name'];
188
+ $list_item['description'] = $item['description'];
189
+
190
+ if ( method_exists( $status, $item['method'] ) ) {
191
+ $list_item['status'] = $status->{$item['method']}();
192
+
193
+ // Do not display status for methods that are depending on connection status to Printful
194
+ if ( ! Printful_Integration::instance()->is_connected(true) && in_array( $item['method'], self::STATUS_CONNECTED_DISPLAYABLE_METHODS ) ) {
195
+ continue;
196
+ }
197
+
198
+ if ( $status->should_result_be_visible( $list_item['status'], $item ) ) {
199
+ $list['items'][] = $list_item;
200
+ }
201
+
202
+ if ( $list_item['status'] == self::PF_STATUS_FAIL || $list_item['status'] == self::PF_STATUS_NOT_CONNECTED ) {
203
+ $list['overall_status'] = false;
204
+ $issueCount ++;
205
+ }
206
+ }
207
+ }
208
+
209
+ set_transient( Printful_Admin_Status::PF_CACHED_CHECKLIST, $list, MINUTE_IN_SECONDS );
210
+ set_transient( Printful_Admin_Status::PF_STATUS_ISSUE_COUNT, $issueCount, HOUR_IN_SECONDS );
211
+
212
+ return $list;
213
+ }
214
+
215
+ /**
216
+ * Execute only one test
217
+ * @param $method
218
+ * @return mixed
219
+ */
220
+ public function run_single_test( $method ) {
221
+ if ( method_exists( $this, $method ) ) {
222
+ return $this->{$method}();
223
+ }
224
+ return false;
225
+ }
226
+
227
+ /**
228
+ * @param $status
229
+ * @param bool $item
230
+ *
231
+ * @return int
232
+ */
233
+ private function should_result_be_visible( $status, $item = false ) {
234
+
235
+ if ( ! isset( $item['silent'] ) || ( $item['silent'] === true && $status === self::PF_STATUS_FAIL ) ) { //silent items are only shown on FAIL
236
+ return true;
237
+ }
238
+
239
+ return false;
240
+ }
241
+
242
+ /**
243
+ * Function for checking if thumbnails are resized
244
+ */
245
+ private function check_uploads_write() {
246
+
247
+ $upload_dir = wp_upload_dir();
248
+ if ( is_writable( $upload_dir['basedir'] ) ) {
249
+ return self::PF_STATUS_OK;
250
+ }
251
+
252
+ return self::PF_STATUS_FAIL;
253
+ }
254
+
255
+ /**
256
+ * @return int
257
+ */
258
+ private function check_PHP_memory_limit() {
259
+
260
+ $memory_limit = ini_get( 'memory_limit' );
261
+
262
+ if ( preg_match( '/^(\d+)(.)$/', $memory_limit, $matches ) ) {
263
+ if ( $matches[2] == 'M' ) {
264
+ $memory_limit = $matches[1] * 1024 * 1024; // nnnM -> nnn MB
265
+ } else if ( $matches[2] == 'K' ) {
266
+ $memory_limit = $matches[1] * 1024; // nnnK -> nnn KB
267
+ }
268
+ }
269
+
270
+ $ok = ( $memory_limit >= 128 * 1024 * 1024 ); // at least 128M?
271
+
272
+ if ( $ok ) {
273
+ return self::PF_STATUS_OK;
274
+ }
275
+
276
+ return self::PF_STATUS_FAIL;
277
+ }
278
+
279
+ /**
280
+ * @return int
281
+ */
282
+ private function check_WP_version() {
283
+
284
+ $current = get_bloginfo( 'version' );
285
+
286
+ try {
287
+ $url = 'https://api.wordpress.org/core/version-check/1.7/';
288
+ $response = wp_remote_get( $url );
289
+
290
+ if ( ! is_wp_error( $response ) ) {
291
+ $json = $response['body'];
292
+ $obj = json_decode( $json );
293
+ }
294
+
295
+ if ( empty( $obj ) ) {
296
+ return self::PF_STATUS_FAIL;
297
+ }
298
+
299
+ $version = $obj->offers[0];
300
+ $latest = $version->version;
301
+
302
+ } catch ( Exception $e ) {
303
+ return self::PF_STATUS_FAIL;
304
+ }
305
+
306
+ if ( ! $latest ) {
307
+ return self::PF_STATUS_FAIL;
308
+ }
309
+
310
+ if ( version_compare( $current, $latest, '>=' ) ) {
311
+ return self::PF_STATUS_OK;
312
+ }
313
+
314
+ return self::PF_STATUS_FAIL;
315
+ }
316
+
317
+ /**
318
+ * @return int
319
+ */
320
+ private function check_PF_webhooks() {
321
+ global $wpdb;
322
+
323
+ // Get the webhooks
324
+ // In version 3.3 the webhooks are stored in separate table, before that they were stored in posts table
325
+ if ( version_compare( WC()->version, '3.3', '<' ) ) {
326
+
327
+ // Query args
328
+ $args = array(
329
+ 'post_type' => 'shop_webhook',
330
+ 'nopaging' => true,
331
+ 'ignore_sticky_posts' => true,
332
+ 's' => self::PF_WEBHOOK_NAME,
333
+ 'post_status' => 'published',
334
+ );
335
+
336
+ $webhook_results = new WP_Query( $args );
337
+ $webhooks = $webhook_results->posts;
338
+ $count = count( $webhooks ) > 0;
339
+ } else {
340
+ $count = $wpdb->get_var(
341
+ $wpdb->prepare( "SELECT COUNT(*) as webhook_count FROM {$wpdb->prefix}wc_webhooks WHERE name = %s",
342
+ self::PF_WEBHOOK_NAME
343
+ ) );
344
+ }
345
+
346
+ if ( $count > 0 ) {
347
+ return self::PF_STATUS_OK;
348
+ }
349
+
350
+ return self::PF_STATUS_FAIL;
351
+ }
352
+
353
+ /**
354
+ * @return int
355
+ */
356
+ private function check_WC_API_access() {
357
+
358
+ global $wpdb;
359
+
360
+ //if any keys are set
361
+ $count = $wpdb->get_var(
362
+ "SELECT COUNT(*) as key_count FROM {$wpdb->prefix}woocommerce_api_keys"
363
+ );
364
+
365
+ if ( $count == 0 ) {
366
+ return self::PF_STATUS_FAIL;
367
+ }
368
+
369
+ // Get the API key with matching description
370
+ $printfulKey = esc_sql( $wpdb->esc_like( wc_clean( self::API_KEY_SEARCH_STRING ) ) );
371
+ $queryArg = '%'. $wpdb->esc_like( $printfulKey ) . '%';
372
+
373
+ $key = $wpdb->get_row(
374
+ $wpdb->prepare(
375
+ "SELECT * FROM {$wpdb->prefix}woocommerce_api_keys WHERE description LIKE %s ORDER BY last_access LIMIT 1",
376
+ $queryArg
377
+ )
378
+ );
379
+
380
+ if ( ! empty( $key ) && $key->permissions == 'read_write' ) {
381
+ return self::PF_STATUS_OK;
382
+ }
383
+
384
+ return self::PF_STATUS_WARNING;
385
+ }
386
+
387
+ /**
388
+ * @return int
389
+ */
390
+ private function check_PF_API_key() {
391
+
392
+ $option = get_option( 'woocommerce_printful_settings', array() );
393
+ if ( ! empty( $option['printful_key'] ) && strlen( $option['printful_key'] ) == 36 ) {
394
+ return self::PF_STATUS_OK;
395
+ }
396
+
397
+ return self::PF_STATUS_FAIL;
398
+ }
399
+
400
+ /**
401
+ * @return int
402
+ * @throws PrintfulException
403
+ */
404
+ private function check_PF_API_connect() {
405
+
406
+ if ( Printful_Integration::instance()->is_connected(true) ) {
407
+ return self::PF_STATUS_OK;
408
+ }
409
+
410
+ return self::PF_STATUS_NOT_CONNECTED;
411
+ }
412
+
413
+ /**
414
+ * @return int
415
+ */
416
+ private function check_PHP_time_limit() {
417
+ $time_limit = ini_get( 'max_execution_time' );
418
+
419
+ if ( !$time_limit || $time_limit >= 30 ) {
420
+ return self::PF_STATUS_OK;
421
+ }
422
+
423
+ return self::PF_STATUS_FAIL;
424
+ }
425
+
426
+ /**
427
+ * @return int
428
+ */
429
+ private function check_PF_sync_errors() {
430
+
431
+ $sync_log = get_option( Printful_Request_log::PF_OPTION_INCOMING_API_REQUEST_LOG, array() );
432
+ if ( empty( $sync_log ) ) {
433
+ return self::PF_STATUS_OK; //no results means no errors
434
+ }
435
+
436
+ $sync_log = array_reverse( $sync_log );
437
+ $sync_log = array_slice( $sync_log, 0, 6 ); //we only care about last to syncs
438
+
439
+ foreach ( $sync_log as $sl ) {
440
+ if ( ! empty( $sl['result'] ) && $sl['result'] == 'ERROR' ) {
441
+ return self::PF_STATUS_FAIL;
442
+ }
443
+ }
444
+
445
+ return self::PF_STATUS_OK;
446
+ }
447
+
448
+ /**
449
+ * @return int
450
+ */
451
+ private function check_W3_db_cache() {
452
+
453
+ if ( ! is_plugin_active( 'w3-total-cache/w3-total-cache.php' ) ) {
454
+ return self::PF_STATUS_OK;
455
+ }
456
+
457
+ $w3tc_config_file = get_home_path() . 'wp-content/w3tc-config/master.php';
458
+ if ( file_exists( $w3tc_config_file ) && is_readable( $w3tc_config_file ) ) {
459
+ $content = @file_get_contents( $w3tc_config_file );
460
+ $config = @json_decode( substr( $content, 14 ), true );
461
+
462
+ if ( is_array( $config ) && ! empty( $config['dbcache.enabled'] ) ) {
463
+ return ! $config['dbcache.enabled'];
464
+ }
465
+ }
466
+
467
+ return self::PF_STATUS_OK;
468
+ }
469
+
470
+ /**
471
+ * @return int
472
+ */
473
+ private function check_permalinks() {
474
+
475
+ $permalinks = get_option( 'permalink_structure', false );
476
+
477
+ if ( $permalinks && strlen( $permalinks ) > 0 ) {
478
+ return self::PF_STATUS_OK;
479
+ }
480
+
481
+ return self::PF_STATUS_FAIL;
482
+ }
483
+
484
+ /**
485
+ * @return int
486
+ */
487
+ private function check_printaura_plugin() {
488
+
489
+ if ( ! is_plugin_active( 'printaura-woocommerce-api/printaura-woocommerce-api.php' ) ) {
490
+ return self::PF_STATUS_OK;
491
+ }
492
+
493
+ return self::PF_STATUS_FAIL;
494
+ }
495
+
496
+ /**
497
+ * @return int
498
+ */
499
+ private function check_wp_spamshield() {
500
+
501
+ if ( ! is_plugin_active( 'wp-spamshield/wp-spamshield.php' ) ) {
502
+ return self::PF_STATUS_OK;
503
+ }
504
+
505
+ return self::PF_STATUS_FAIL;
506
+ }
507
+
508
+ /**
509
+ * @return int
510
+ */
511
+ private function check_remote_requests() {
512
+
513
+ // Setup request args.
514
+ $http_args = array(
515
+ 'method' => 'POST',
516
+ 'timeout' => MINUTE_IN_SECONDS,
517
+ 'redirection' => 0,
518
+ 'httpversion' => '1.0',
519
+ 'blocking' => true,
520
+ 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', WC_VERSION, $GLOBALS['wp_version'] ),
521
+ 'body' => trim( json_encode( array( 'test' => true ) ) ),
522
+ 'headers' => array( 'Content-Type' => 'application/json' ),
523
+ 'cookies' => array(),
524
+ );
525
+
526
+ // Add custom headers.
527
+ $http_args['headers']['X-WC-Webhook-Source'] = home_url( '/' ); // Since 2.6.0.
528
+ $http_args['headers']['X-WC-Webhook-Topic'] = self::PF_REMOTE_REQUEST_TOPIC;
529
+
530
+ // Webhook away!
531
+ $response = wp_safe_remote_request( Printful_Base::get_printful_api_host() . self::PF_REMOTE_REQUEST_URL, $http_args );
532
+
533
+ if ( is_wp_error( $response ) ) {
534
+ return self::PF_STATUS_FAIL;
535
+ }
536
+
537
+ return self::PF_STATUS_OK;
538
+ }
539
+
540
+ /**
541
+ * @return int
542
+ */
543
+ private function check_WC_auth_url_access() {
544
+ $url = home_url( '/' ) . 'wc-auth/v1/authorize?app_name=Printful&scope=read_write&user_id=1&return_url=https%3A%2F%2Fwww.printful.com%2Fdashboard%2Fwoocommerce%2Freturn&callback_url=https%3A%2F%2Fapi.printful.com%2Fhook%2Fwoocommerce-auth-callback';
545
+ $http_args = array(
546
+ 'timeout' => 60,
547
+ 'method' => 'GET',
548
+ 'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.82 Safari/537.36',
549
+ );
550
+
551
+ $response = wp_safe_remote_get( $url, $http_args );
552
+
553
+ if ( is_wp_error( $response ) ) {
554
+ return self::PF_STATUS_FAIL;
555
+ }
556
+
557
+ $code = $response['response']['code'];
558
+
559
+ if ( $code == 200 ) {
560
+ return self::PF_STATUS_OK;
561
+ }
562
+
563
+ return self::PF_STATUS_FAIL;
564
+ }
565
+
566
+ /**
567
+ * @return int
568
+ */
569
+ private function check_site_url_redirect()
570
+ {
571
+
572
+ $regular_site_url_req = wp_remote_head( get_option( 'siteurl' ) . '/wp-json/', array('redirection' => 0));
573
+
574
+ //need to not trigger issues for subfolder wordpress setups
575
+ $slashed_site_url_req = wp_remote_head( trailingslashit(get_option( 'siteurl' )), array('redirection' => 0));
576
+
577
+ if (is_wp_error($regular_site_url_req) || is_wp_error($slashed_site_url_req)) {
578
+ return self::PF_STATUS_FAIL;
579
+ }
580
+
581
+ /** @var WP_HTTP_Requests_Response $response */
582
+ $regular_response = $regular_site_url_req['http_response'];
583
+
584
+ /** @var WP_HTTP_Requests_Response $slashed_response */
585
+ $slashed_response = $slashed_site_url_req['http_response'];
586
+
587
+ if ($regular_response->get_status() == 200 || $slashed_response->get_status() == 200) {
588
+ return self::PF_STATUS_OK;
589
+ }
590
+
591
+ if (in_array($regular_response->get_status(), array(301, 302, 303, 307))) {
592
+ return self::PF_STATUS_FAIL;
593
+ }
594
+
595
+ return self::PF_STATUS_WARNING;
596
+ }
597
+ }
trunk/includes/class-printful-admin-support.php ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Admin_Support {
5
+
6
+ public static $_instance;
7
+
8
+ /**
9
+ * @return Printful_Admin_Support
10
+ */
11
+ public static function instance() {
12
+
13
+ if ( is_null( self::$_instance ) ) {
14
+ self::$_instance = new self();
15
+ }
16
+
17
+ return self::$_instance;
18
+ }
19
+
20
+ /**
21
+ * Setup the view
22
+ */
23
+ public static function view() {
24
+
25
+ $support = self::instance();
26
+ $support->render();
27
+ }
28
+
29
+ /**
30
+ * Display support report
31
+ */
32
+ public function render() {
33
+
34
+ Printful_Admin::load_template( 'header', array( 'tabs' => Printful_Admin::get_tabs() ) );
35
+
36
+ Printful_Admin::load_template( 'ajax-loader', array( 'action' => 'get_printful_status_report', 'message' => __( 'Building support report (this may take up to 30 seconds)...', 'printful' ) ) );
37
+
38
+ Printful_Admin::load_template( 'support-info' );
39
+
40
+ Printful_Admin::load_template( 'footer' );
41
+ }
42
+
43
+ /**
44
+ * Build the content for status page
45
+ */
46
+ public static function render_status_report_ajax() {
47
+
48
+ $status_report = self::instance()->generate_report();
49
+ Printful_Admin::load_template( 'status-report', array( 'status_report' => $status_report ) );
50
+
51
+ exit;
52
+ }
53
+
54
+ /**
55
+ * Create system status report
56
+ * @return string
57
+ * @throws PrintfulException
58
+ */
59
+ public function generate_report() {
60
+
61
+ if ( ! class_exists( 'WC_REST_System_Status_Controller' ) ) {
62
+ return false;
63
+ }
64
+
65
+ $system_status = new WC_REST_System_Status_Controller; //make use of the woocommerce system status report
66
+
67
+ ob_start();
68
+
69
+ echo __( "##### Printful Checklist #####\n", 'printful' );
70
+ $checklist = Printful_Admin_Status::get_checklist();
71
+ foreach ( $checklist['items'] as $item ) {
72
+ $status = 'OK';
73
+ if($item['status'] == Printful_Admin_Status::PF_STATUS_WARNING) {
74
+ $status = 'WARNING';
75
+ } else if($item['status'] == Printful_Admin_Status::PF_STATUS_FAIL) {
76
+ $status = 'FAIL';
77
+ } else if ($item['status'] == Printful_Admin_Status::PF_STATUS_NOT_CONNECTED) {
78
+ $status = 'NOT CONNECTED';
79
+ }
80
+ echo "* ";
81
+ echo esc_html( str_pad( esc_html( $item['name'] ), 30 ) ) . '=> ' . esc_html( $status ) . "\n";
82
+ }
83
+
84
+ echo "\n\n##### Printful Last Sync's #####\n";
85
+ $syncReport = $this->get_sync_report();
86
+ if ( ! empty( $syncReport ) ) {
87
+ echo esc_html( str_pad( 'Date', 30 ) );
88
+ echo esc_html( str_pad( 'Request', 30 ) );
89
+ echo esc_html( str_pad( 'Message', 30 ) );
90
+ echo "\n";
91
+
92
+ foreach ( $syncReport as $sr ) {
93
+ echo "* ";
94
+ echo esc_html( str_pad( $sr['date'] . ';', 30 ) );
95
+ echo esc_html( str_pad( $sr['path'] . ';', 30 ) );
96
+ echo esc_html( str_pad( $sr['message'] . ';', 30 ) );
97
+ echo "\n";
98
+ }
99
+ }
100
+
101
+ echo "\n\n##### Environment #####\n";
102
+ $this->output_report_block( $system_status->get_environment_info() );
103
+
104
+ echo "\n\n##### Database #####\n";
105
+ $this->output_report_block( $system_status->get_database_info() );
106
+
107
+ echo "\n\n##### Active Plugins #####\n";
108
+ foreach ( $system_status->get_active_plugins() as $plugin ) {
109
+ if ( ! empty( $plugin['name'] ) ) {
110
+ echo "* ";
111
+ echo esc_html( $plugin['name'] ) . " (" . esc_html( $plugin['version'] ) . ")\n";
112
+ }
113
+ }
114
+
115
+ echo "\n\n##### Theme #####\n";
116
+ $this->output_report_block( $system_status->get_theme_info() );
117
+
118
+ echo "\n\n##### WooCommerce settings #####\n";
119
+ $this->output_report_block( $system_status->get_settings() );
120
+
121
+ if (
122
+ ( defined( 'WP_DEBUG' ) && WP_DEBUG == true )
123
+ &&
124
+ ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG == true )
125
+ ) {
126
+ echo "\n\n##### Wordpress Error log (last 50 entries) #####\n";
127
+ $contents = $this->get_error_log_contents();
128
+ if ( $contents ) {
129
+ print_r( $contents );
130
+ }
131
+ }
132
+
133
+ $report = ob_get_contents();
134
+ ob_end_clean();
135
+
136
+ return $report;
137
+ }
138
+
139
+ /**
140
+ * Get last 50 lines of error log
141
+ * @return bool|string
142
+ */
143
+ public function get_error_log_contents() {
144
+
145
+ if ( ! function_exists( 'fopen' ) ) {
146
+ return false;
147
+ }
148
+
149
+ return $this->file_tail( WP_CONTENT_DIR . '/debug.log', 50 );
150
+ }
151
+
152
+ /**
153
+ * source: https://gist.github.com/lorenzos/1711e81a9162320fde20
154
+ * @param $filepath
155
+ * @param int $lines
156
+ * @param bool $adaptive
157
+ *
158
+ * @return bool|string
159
+ */
160
+ function file_tail( $filepath, $lines = 1, $adaptive = true ) {
161
+
162
+ $f = @fopen( $filepath, "rb" );
163
+ if ( $f === false ) {
164
+ return false;
165
+ }
166
+
167
+ // Sets buffer size, according to the number of lines to retrieve.
168
+ if ( ! $adaptive ) {
169
+ $buffer = 4096;
170
+ } else {
171
+ $buffer = ( $lines < 2 ? 64 : ( $lines < 10 ? 512 : 4096 ) );
172
+ }
173
+
174
+ // Jump to last character
175
+ fseek( $f, - 1, SEEK_END );
176
+ if ( fread( $f, 1 ) != "\n" ) {
177
+ $lines -= 1;
178
+ }
179
+
180
+ $output = '';
181
+ $chunk = '';
182
+ while ( ftell( $f ) > 0 && $lines >= 0 ) {
183
+ // Figure out how far back we should jump
184
+ $seek = min( ftell( $f ), $buffer );
185
+ // Do the jump (backwards, relative to where we are)
186
+ fseek( $f, - $seek, SEEK_CUR );
187
+ $output = ( $chunk = fread( $f, $seek ) ) . $output;
188
+ fseek( $f, - mb_strlen( $chunk, '8bit' ), SEEK_CUR );
189
+ $lines -= substr_count( $chunk, "\n" );
190
+ }
191
+ while ( $lines ++ < 0 ) {
192
+ $output = substr( $output, strpos( $output, "\n" ) + 1 );
193
+ }
194
+ fclose( $f );
195
+
196
+ return trim( $output );
197
+ }
198
+
199
+
200
+ /**
201
+ * Displays the data
202
+ * @param $data
203
+ */
204
+ public function output_report_block( $data ) {
205
+
206
+ foreach ( $data as $key => $item ) {
207
+ if ( is_string( $item ) ) {
208
+ echo "* ";
209
+ echo esc_html( str_pad($key, 30) ) . "=> " . esc_html($item) . "\n";
210
+ }
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Returns log of last incoming API requests from Printful
216
+ * @return array
217
+ */
218
+ public function get_sync_report() {
219
+
220
+ $report = array();
221
+ $request_log = get_option( Printful_Request_log::PF_OPTION_INCOMING_API_REQUEST_LOG, array() );
222
+ $request_log = array_reverse( $request_log );
223
+
224
+ if ( empty( $request_log ) ) {
225
+ return $report;
226
+ }
227
+
228
+ foreach ( $request_log as $log ) {
229
+ $report[] = array(
230
+ 'date' => $log['date'],
231
+ 'path' => $log['request'],
232
+ 'message' => $log['result'],
233
+ );
234
+ }
235
+
236
+ return $report;
237
+ }
238
+ }
trunk/includes/class-printful-admin.php ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Admin {
5
+
6
+ const MENU_TITLE_TOP = 'Printful';
7
+ const PAGE_TITLE_DASHBOARD = 'Dashboard';
8
+ const MENU_TITLE_DASHBOARD = 'Dashboard';
9
+ const MENU_SLUG_DASHBOARD = 'printful-dashboard';
10
+ const CAPABILITY = 'manage_options';
11
+
12
+ public static function init() {
13
+ $admin = new self;
14
+ $admin->register_admin();
15
+ }
16
+
17
+ /**
18
+ * Register admin scripts
19
+ */
20
+ public function register_admin() {
21
+
22
+ add_action( 'admin_menu', array( $this, 'register_admin_menu_page' ) );
23
+ add_action( 'admin_enqueue_scripts', array( $this, 'add_admin_styles' ) );
24
+ add_action( 'admin_enqueue_scripts', array( $this, 'add_admin_scripts' ) );
25
+ add_action( 'wp_enqueue_scripts', array( $this, 'add_global_style' ) );
26
+ add_action( 'admin_bar_menu', array( $this, 'add_printful_status_toolbar' ), 999 );
27
+ }
28
+
29
+ /**
30
+ * Loads stylesheets used in printful admin pages
31
+ * @param $hook
32
+ */
33
+ public function add_admin_styles($hook) {
34
+
35
+ wp_enqueue_style( 'printful-global', plugins_url( '../assets/css/global.css', __FILE__ ) );
36
+
37
+ if ( strpos( $hook, 'printful-dashboard' ) !== false ) {
38
+ wp_enqueue_style( 'wp-color-picker' );
39
+ wp_enqueue_style( 'printful-dashboard', plugins_url( '../assets/css/dashboard.css', __FILE__ ) );
40
+ wp_enqueue_style( 'printful-status', plugins_url( '../assets/css/status.css', __FILE__ ) );
41
+ wp_enqueue_style( 'printful-support', plugins_url( '../assets/css/support.css', __FILE__ ) );
42
+ wp_enqueue_style( 'printful-settings', plugins_url( '../assets/css/settings.css', __FILE__ ) );
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Loads stylesheet for printful toolbar element
48
+ */
49
+ public function add_global_style() {
50
+ if ( is_user_logged_in() ) {
51
+ wp_enqueue_style( 'printful-global', plugins_url( '../assets/css/global.css', __FILE__ ) );
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Loads scripts used in printful admin pages
57
+ * @param $hook
58
+ */
59
+ public function add_admin_scripts($hook) {
60
+ if ( strpos( $hook, 'printful-dashboard' ) !== false ) {
61
+ wp_enqueue_script( 'wp-color-picker' );
62
+ wp_enqueue_script( 'printful-settings', plugins_url( '../assets/js/settings.js', __FILE__ ) );
63
+ wp_enqueue_script( 'printful-connect', plugins_url( '../assets/js/connect.js', __FILE__ ) );
64
+ wp_enqueue_script( 'printful-block-loader', plugins_url( '../assets/js/block-loader.js', __FILE__ ) );
65
+ wp_enqueue_script( 'printful-intercom', plugins_url( '../assets/js/intercom.min.js', __FILE__ ) );
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Register admin menu pages
71
+ */
72
+ public function register_admin_menu_page() {
73
+
74
+ add_menu_page(
75
+ __( 'Dashboard', 'printful' ),
76
+ self::MENU_TITLE_TOP,
77
+ self::CAPABILITY,
78
+ self::MENU_SLUG_DASHBOARD,
79
+ array( 'Printful_Admin', 'route' ),
80
+ Printful_Base::get_asset_url() . 'images/printful-menu-icon.png',
81
+ 58
82
+ );
83
+ }
84
+
85
+ /**
86
+ * Route the tabs
87
+ */
88
+ public static function route() {
89
+
90
+ $tabs = array(
91
+ 'dashboard' => 'Printful_Admin_Dashboard',
92
+ 'settings' => 'Printful_Admin_Settings',
93
+ 'status' => 'Printful_Admin_Status',
94
+ 'support' => 'Printful_Admin_Support',
95
+ );
96
+
97
+ $tab = ( ! empty( $_GET['tab'] ) ? $_GET['tab'] : 'dashboard' );
98
+ if ( ! empty( $tabs[ $tab ] ) ) {
99
+ call_user_func( array( $tabs[ $tab ], 'view' ) );
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get the tabs used in printful admin pages
105
+ * @return array
106
+ * @throws PrintfulException
107
+ */
108
+ public static function get_tabs() {
109
+
110
+ $tabs = array(
111
+ array( 'name' => __( 'Settings', 'printful' ), 'tab_url' => 'settings' ),
112
+ array( 'name' => __( 'Status', 'printful' ), 'tab_url' => 'status' ),
113
+ array( 'name' => __( 'Support', 'printful' ), 'tab_url' => 'support' ),
114
+ );
115
+
116
+ if ( Printful_Integration::instance()->is_connected() ) {
117
+ array_unshift( $tabs, array( 'name' => __( 'Dashboard', 'printful' ), 'tab_url' => false ) );
118
+ } else {
119
+ array_unshift( $tabs, array( 'name' => __( 'Connect', 'printful' ), 'tab_url' => false ) );
120
+ }
121
+
122
+ return $tabs;
123
+ }
124
+
125
+ /**
126
+ * Create the printful toolbar
127
+ * @param $wp_admin_bar
128
+ */
129
+ public function add_printful_status_toolbar( $wp_admin_bar ) {
130
+
131
+ $issueCount = get_transient( Printful_Admin_Status::PF_STATUS_ISSUE_COUNT );
132
+
133
+ if ( $issueCount ) {
134
+ //Add top level menu item
135
+ $args = array(
136
+ 'id' => 'printful_toolbar',
137
+ 'title' => 'Printful Integration' . ( $issueCount > 0 ? ' <span class="printful-toolbar-issues">' . esc_attr( $issueCount ) . '</span>' : '' ),
138
+ 'href' => get_admin_url( null, 'admin.php?page=' . Printful_Admin::MENU_SLUG_DASHBOARD ),
139
+ 'meta' => array( 'class' => 'printful-toolbar' ),
140
+ );
141
+ $wp_admin_bar->add_node( $args );
142
+
143
+ //Add status
144
+ $args = array(
145
+ 'id' => 'printful_toolbar_status',
146
+ 'parent' => 'printful_toolbar',
147
+ 'title' => 'Integration status' . ( $issueCount > 0 ? ' (' . esc_attr( $issueCount ) . _n( ' issue', ' issues', $issueCount ) . ')' : '' ),
148
+ 'href' => get_admin_url( null, 'admin.php?page=' . Printful_Admin::MENU_SLUG_DASHBOARD . '&tab=status' ),
149
+ 'meta' => array( 'class' => 'printful-toolbar-status' ),
150
+ );
151
+ $wp_admin_bar->add_node( $args );
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Load a template file. Extract any variables that are passed
157
+ * @param $name
158
+ * @param array $variables
159
+ */
160
+ public static function load_template( $name, $variables = array() ) {
161
+
162
+ if ( ! empty( $variables ) ) {
163
+ extract( $variables );
164
+ }
165
+
166
+ $filename = plugin_dir_path( __FILE__ ) . 'templates/' . $name . '.php';
167
+ if ( file_exists( $filename ) ) {
168
+ include( $filename );
169
+ }
170
+ }
171
+
172
+ }
trunk/includes/class-printful-carriers.php ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Carriers {
5
+
6
+ public $carriers;
7
+ public static $_instance;
8
+
9
+ public static function instance() {
10
+
11
+ if ( is_null( self::$_instance ) ) {
12
+ self::$_instance = new self();
13
+ }
14
+
15
+ return self::$_instance;
16
+ }
17
+
18
+ public function __construct() {
19
+
20
+ $this->carriers = $this->get_carriers();
21
+ }
22
+
23
+ /**
24
+ * Get carrier data
25
+ * @return mixed
26
+ */
27
+ public function get_carriers() {
28
+ $carriers = get_transient( 'printful_carriers' );
29
+ if ( ! $carriers ) {
30
+ $carriers = $this->refresh_carriers();
31
+ }
32
+
33
+ return $carriers;
34
+ }
35
+
36
+ /**
37
+ * Refresh carrier data from Printful
38
+ * @return mixed
39
+ */
40
+ public function refresh_carriers() {
41
+
42
+ try {
43
+ $carriers = Printful_Integration::instance()->get_client()->get( 'store/get-shipping-methods' );
44
+ $this->update_carrier_cache( $carriers );
45
+ } catch (PrintfulApiException $e) {
46
+ $carriers = array();
47
+ } catch (PrintfulException $e) {
48
+ $carriers = array();
49
+ }
50
+
51
+ return $carriers;
52
+ }
53
+
54
+ /**
55
+ * Update carrier transient
56
+ * @param $carriers
57
+ */
58
+ public function update_carrier_cache($carriers) {
59
+
60
+ set_transient( 'printful_carriers', $carriers, MINUTE_IN_SECONDS * 5); //5mins
61
+ }
62
+
63
+ /**
64
+ * Post carrier settings to printful
65
+ * @param $data
66
+ * @return mixed
67
+ */
68
+ public function post_carriers( $data ) {
69
+
70
+ if ( empty( $data ) ) {
71
+ return false;
72
+ }
73
+
74
+ $shipping = new self;
75
+ try {
76
+ $carriers = Printful_Integration::instance()->get_client()->patch( 'store/update-shipping-methods', $data );
77
+
78
+ if ( empty( $carriers['error'] ) ) {
79
+ $shipping->update_carrier_cache( $carriers );
80
+ }
81
+ } catch ( PrintfulApiException $e ) {
82
+ $carriers = false;
83
+ } catch ( PrintfulException $e ) {
84
+ $carriers = false;
85
+ }
86
+
87
+ return $carriers;
88
+ }
89
+
90
+ }
trunk/includes/class-printful-client.php ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ /**
5
+ * Printful API client
6
+ */
7
+ class Printful_Client {
8
+
9
+ private $key = false;
10
+ private $lastResponseRaw;
11
+ private $lastResponse;
12
+ private $userAgent = 'Printful WooCommerce Plugin';
13
+ private $apiUrl;
14
+
15
+ /**
16
+ * @param string $key Printful Store API key
17
+ * @param bool|string $disable_ssl Force HTTP instead of HTTPS for API requests
18
+ *
19
+ * @throws PrintfulException if the library failed to initialize
20
+ */
21
+ public function __construct( $key = '', $disable_ssl = false ) {
22
+
23
+ $key = (string) $key;
24
+
25
+ $this->userAgent .= ' ' . Printful_Base::VERSION . ' (WP ' . get_bloginfo( 'version' ) . ' + WC ' . WC()->version . ')';
26
+
27
+ if ( ! function_exists( 'json_decode' ) || ! function_exists( 'json_encode' ) ) {
28
+ throw new PrintfulException( 'PHP JSON extension is required for the Printful API library to work!' );
29
+ }
30
+ if ( strlen( $key ) < 32 ) {
31
+ throw new PrintfulException( 'Missing or invalid Printful store key!' );
32
+ }
33
+ $this->key = $key;
34
+
35
+ if ( $disable_ssl ) {
36
+ $this->apiUrl = str_replace( 'https://', 'http://', $this->apiUrl );
37
+ }
38
+
39
+ //setup api host
40
+ $this->apiUrl = Printful_Base::get_printful_api_host();
41
+ }
42
+
43
+ /**
44
+ * Returns total available item count from the last request if it supports paging (e.g order list) or null otherwise.
45
+ *
46
+ * @return int|null Item count
47
+ */
48
+ public function getItemCount() {
49
+ return isset( $this->lastResponse['paging']['total'] ) ? $this->lastResponse['paging']['total'] : null;
50
+ }
51
+
52
+ /**
53
+ * Perform a GET request to the API
54
+ * @param string $path Request path (e.g. 'orders' or 'orders/123')
55
+ * @param array $params Additional GET parameters as an associative array
56
+ * @return mixed API response
57
+ * @throws PrintfulApiException if the API call status code is not in the 2xx range
58
+ * @throws PrintfulException if the API call has failed or the response is invalid
59
+ */
60
+ public function get( $path, $params = array() ) {
61
+ return $this->request( 'GET', $path, $params );
62
+ }
63
+
64
+ /**
65
+ * Perform a DELETE request to the API
66
+ * @param string $path Request path (e.g. 'orders' or 'orders/123')
67
+ * @param array $params Additional GET parameters as an associative array
68
+ * @return mixed API response
69
+ * @throws PrintfulApiException if the API call status code is not in the 2xx range
70
+ * @throws PrintfulException if the API call has failed or the response is invalid
71
+ */
72
+ public function delete( $path, $params = array() ) {
73
+ return $this->request( 'DELETE', $path, $params );
74
+ }
75
+
76
+ /**
77
+ * Perform a POST request to the API
78
+ * @param string $path Request path (e.g. 'orders' or 'orders/123')
79
+ * @param array $data Request body data as an associative array
80
+ * @param array $params Additional GET parameters as an associative array
81
+ * @return mixed API response
82
+ * @throws PrintfulApiException if the API call status code is not in the 2xx range
83
+ * @throws PrintfulException if the API call has failed or the response is invalid
84
+ */
85
+ public function post( $path, $data = array(), $params = array() ) {
86
+ return $this->request( 'POST', $path, $params, $data );
87
+ }
88
+ /**
89
+ * Perform a PUT request to the API
90
+ * @param string $path Request path (e.g. 'orders' or 'orders/123')
91
+ * @param array $data Request body data as an associative array
92
+ * @param array $params Additional GET parameters as an associative array
93
+ * @return mixed API response
94
+ * @throws PrintfulApiException if the API call status code is not in the 2xx range
95
+ * @throws PrintfulException if the API call has failed or the response is invalid
96
+ */
97
+ public function put( $path, $data = array(), $params = array() ) {
98
+ return $this->request( 'PUT', $path, $params, $data );
99
+ }
100
+
101
+
102
+ /**
103
+ * Perform a PATCH request to the API
104
+ * @param string $path Request path
105
+ * @param array $data Request body data as an associative array
106
+ * @param array $params
107
+ * @return mixed API response
108
+ * @throws PrintfulApiException if the API call status code is not in the 2xx range
109
+ * @throws PrintfulException if the API call has failed or the response is invalid
110
+ */
111
+ public function patch( $path, $data = array(), $params = array() )
112
+ {
113
+ return $this->request( 'PATCH', $path, $params, $data );
114
+ }
115
+
116
+ /**
117
+ * Return raw response data from the last request
118
+ * @return string|null Response data
119
+ */
120
+ public function getLastResponseRaw() {
121
+ return $this->lastResponseRaw;
122
+ }
123
+ /**
124
+ * Return decoded response data from the last request
125
+ * @return array|null Response data
126
+ */
127
+ public function getLastResponse() {
128
+ return $this->lastResponse;
129
+ }
130
+
131
+ /**
132
+ * Internal request implementation
133
+ *
134
+ * @param $method
135
+ * @param $path
136
+ * @param array $params
137
+ * @param null $data
138
+ *
139
+ * @return
140
+ * @throws PrintfulApiException
141
+ * @throws PrintfulException
142
+ */
143
+ private function request( $method, $path, array $params = array(), $data = null ) {
144
+
145
+ $this->lastResponseRaw = null;
146
+ $this->lastResponse = null;
147
+
148
+ $url = trim( $path, '/' );
149
+
150
+ if ( ! empty( $params ) ) {
151
+ $url .= '?' . http_build_query( $params );
152
+ }
153
+
154
+ $request = array(
155
+ 'timeout' => 10,
156
+ 'user-agent' => $this->userAgent,
157
+ 'method' => $method,
158
+ 'headers' => array( 'Authorization' => 'Basic ' . base64_encode( $this->key ) ),
159
+ 'body' => $data !== null ? json_encode( $data ) : null,
160
+ );
161
+
162
+ $result = wp_remote_get( $this->apiUrl . $url, $request );
163
+
164
+ //allow other methods to hook in on the api result
165
+ $result = apply_filters( 'printful_api_result', $result, $method, $this->apiUrl . $url, $request );
166
+
167
+ if ( is_wp_error( $result ) ) {
168
+ throw new PrintfulException( "API request failed - " . $result->get_error_message() );
169
+ }
170
+ $this->lastResponseRaw = $result['body'];
171
+ $this->lastResponse = $response = json_decode( $result['body'], true );
172
+
173
+ if ( ! isset( $response['code'], $response['result'] ) ) {
174
+ throw new PrintfulException( 'Invalid API response' );
175
+ }
176
+ $status = (int) $response['code'];
177
+ if ( $status < 200 || $status >= 300 ) {
178
+ throw new PrintfulApiException( (string) $response['result'], $status );
179
+ }
180
+
181
+ return $response['result'];
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Class PrintfulException Generic Printful exception
187
+ */
188
+ class PrintfulException extends Exception {}
189
+ /**
190
+ * Class PrintfulException Printful exception returned from the API
191
+ */
192
+ class PrintfulApiException extends PrintfulException {}
trunk/includes/class-printful-customizer.php ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Customizer {
5
+
6
+ /**
7
+ * Initialize the values, hooks and actions
8
+ */
9
+ public static function init()
10
+ {
11
+ $customizer = new self;
12
+
13
+ $customizer->hook_actions();
14
+ }
15
+
16
+ public function hook_actions()
17
+ {
18
+ // Save customized product as independent cart item data
19
+ add_filter( 'woocommerce_add_cart_item_data', array( $this, 'save_customizer_hash_data_to_cart' ), 10, 1 );
20
+ add_filter( 'woocommerce_cart_item_thumbnail', array( $this, 'change_woocommerce_cart_item_thumbnail' ), 20, 2 );
21
+ // add property to order
22
+ add_action('woocommerce_new_order_item', array( $this, 'save_customizer_hash_order_meta' ), 10, 2 );
23
+
24
+ // hide hash from meta data provided to user
25
+ add_filter( 'woocommerce_order_item_get_formatted_meta_data', array( $this, 'hide_hash_from_meta_data'), 10, 1 );
26
+ add_action( 'wp_ajax_printful_customized_thumb', array( $this, 'printful_customized_thumb' ) );
27
+ add_action( 'wp_ajax_nopriv_printful_customized_thumb', array( $this, 'printful_customized_thumb' ) );
28
+
29
+ add_action( 'wp_enqueue_scripts', array( $this, 'load_customizer_scripts' ));
30
+ }
31
+
32
+ /**
33
+ * Load customizer scripts for cart and product page
34
+ */
35
+ public function load_customizer_scripts() {
36
+ global $post;
37
+
38
+ $is_customized_product = $post && get_post_meta( $post->ID, 'pf_customizable', true );
39
+ if ($is_customized_product || is_cart()) {
40
+ $customizer_script_handle = 'printful-product-customizer';
41
+ wp_enqueue_script( $customizer_script_handle, plugins_url( '../assets/js/product-customizer.js', __FILE__ ) );
42
+ wp_localize_script( $customizer_script_handle, 'pfGlobalCustomizer', array(
43
+ 'admin_url' => esc_url(admin_url()),
44
+ 'modal_title' => Printful_Integration::instance()
45
+ ->get_option( 'pfc_modal_title', Printful_Admin_Settings::DEFAULT_PERSONALIZE_MODAL_TITLE )
46
+ ));
47
+ }
48
+ }
49
+
50
+ /**
51
+ * @param $cart_item_data
52
+ * @return mixed
53
+ */
54
+ public function save_customizer_hash_data_to_cart( $cart_item_data ) {
55
+ if ( isset( $_POST['pfc_hash'] ) && ! empty( $_POST['pfc_hash'] ) ) {
56
+ // Set the custom data in the cart item
57
+ $cart_item_data['pfc_hash'] = (string) sanitize_text_field( $_POST['pfc_hash'] );
58
+ // set pending actual image property, so it is not reloaded each time
59
+ $cart_item_data['customizer_image_generated'] = false;
60
+
61
+ // Make each item as a unique separated cart item
62
+ $cart_item_data['unique_key'] = md5( microtime().rand() );
63
+ }
64
+
65
+ return $cart_item_data;
66
+ }
67
+
68
+ /**
69
+ * @param $thumbnail
70
+ * @param $cart_item
71
+ * @return string
72
+ */
73
+ public function change_woocommerce_cart_item_thumbnail( $thumbnail, $cart_item )
74
+ {
75
+ if ( $cart_item
76
+ && !empty( $cart_item['pfc_hash'] )
77
+ && empty( $cart_item['customizer_image_generated'] )
78
+ ) {
79
+ $class = 'attachment-shop_thumbnail wp-post-image'; // Default cart thumbnail class.
80
+ $src = esc_url( Printful_Base::get_asset_url() . 'images/loading.gif' );
81
+ // Construct img tag.
82
+ return "<img src='{$src}' class='{$class} pf-image-pending' data-hash='{$cart_item['pfc_hash']}' />";
83
+ } else if ($cart_item
84
+ && !empty( $cart_item['pfc_hash'] )
85
+ && !empty( $cart_item['customizer_image_generated'] )
86
+ && empty( $cart_item['customizer_image_loaded'] )
87
+ && !empty( $cart_item['customizer_image'] )
88
+ ) {
89
+ $class = 'attachment-shop_thumbnail wp-post-image'; // Default cart thumbnail class.
90
+ // Construct img tag.
91
+ return "<img src='{$cart_item['customizer_image']}' class='{$class}' data-hash='{$cart_item['pfc_hash']}' />";
92
+ }
93
+
94
+ return $thumbnail;
95
+ }
96
+
97
+ /**
98
+ * @param $itemId
99
+ * @param $item
100
+ *
101
+ * @throws Exception
102
+ */
103
+ public function save_customizer_hash_order_meta( $itemId, $item )
104
+ {
105
+ if (empty($item->legacy_values)) {
106
+ return;
107
+ }
108
+
109
+ if ( isset( $item->legacy_values['pfc_hash'] ) ) {
110
+ wc_add_order_item_meta( $itemId, 'pfc_hash', $item->legacy_values['pfc_hash'] );
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @throws PrintfulException
116
+ * @throws Exception
117
+ */
118
+ public static function printful_customized_thumb()
119
+ {
120
+ if ( empty( $_GET['hashes'] ) ) {
121
+ return;
122
+ }
123
+
124
+ $client = Printful_Integration::instance()->get_client();
125
+ $hashes = implode(',', $_GET['hashes']);
126
+ $response = $client->get('woocommerce/get-hash-images', array(
127
+ 'hashes' => $hashes
128
+ ));
129
+
130
+ // global $woocommerce;
131
+ $items = WC()->cart->get_cart();
132
+
133
+ foreach ($items as $key => $item) {
134
+ if ( isset( $item['pfc_hash'] ) ) {
135
+ foreach ($response as $hash => $image_url) {
136
+ if ($hash !== $item['pfc_hash']) {
137
+ continue;
138
+ }
139
+
140
+ WC()->cart->cart_contents[$key]['customizer_image_generated'] = true;
141
+ WC()->cart->cart_contents[$key]['customizer_image'] = $image_url;
142
+ WC()->cart->cart_contents[$key]['customizer_image_loaded'] = false;
143
+ }
144
+ }
145
+ }
146
+
147
+ WC()->cart->set_session();
148
+
149
+ echo json_encode($response);
150
+ exit;
151
+ }
152
+
153
+ /**
154
+ * Hide pfc_hash from UI
155
+ * @param array $formatted_meta
156
+ * @return mixed
157
+ */
158
+ public function hide_hash_from_meta_data( $formatted_meta )
159
+ {
160
+ foreach( $formatted_meta as $key => $meta ) {
161
+ if ( in_array( $meta->key, array( 'pfc_hash' ) ) ) {
162
+ unset( $formatted_meta[$key] );
163
+ }
164
+ }
165
+
166
+ return $formatted_meta;
167
+ }
168
+ }
trunk/includes/class-printful-integration.php ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Integration
5
+ {
6
+ const PF_API_CONNECT_STATUS = 'printful_api_connect_status';
7
+ const PF_CONNECT_ERROR = 'printful_connect_error';
8
+
9
+ public static $_instance;
10
+
11
+ public static function instance() {
12
+ if ( is_null( self::$_instance ) ) {
13
+ self::$_instance = new self();
14
+ }
15
+
16
+ return self::$_instance;
17
+ }
18
+
19
+ public function __construct() {
20
+ self::$_instance = $this;
21
+ }
22
+
23
+ /**
24
+ * @return Printful_Client
25
+ * @throws PrintfulException
26
+ */
27
+ public function get_client() {
28
+
29
+ require_once 'class-printful-client.php';
30
+ $client = new Printful_Client( $this->get_option( 'printful_key' ), $this->get_option( 'disable_ssl' ) == 'yes' );
31
+
32
+ return $client;
33
+ }
34
+
35
+ /**
36
+ * Check if the connection to printful is working
37
+ * @param bool $force
38
+ * @return bool
39
+ * @throws PrintfulException
40
+ */
41
+ public function is_connected( $force = false ) {
42
+
43
+ $api_key = $this->get_option( 'printful_key' );
44
+
45
+ //dont need to show error - the plugin is simply not setup
46
+ if ( empty( $api_key ) ) {
47
+ return false;
48
+ }
49
+
50
+ //validate length, show error
51
+ if ( strlen( $api_key ) != 36 ) {
52
+ $message = 'Invalid API key - the key must be 36 characters long. Please ensure that your API key in <a href="%s">Settings</a> matches the one in your <a href="%s">Printful dashboard</a>.';
53
+ $settings_url = admin_url( 'admin.php?page=printful-dashboard&tab=settings' );
54
+ $printful_url = Printful_Base::get_printful_host() . 'dashboard/';
55
+ $this->set_connect_error(sprintf( $message, $settings_url, $printful_url ) );
56
+
57
+ return false;
58
+ }
59
+
60
+ //show connect status from cache
61
+ if ( ! $force ) {
62
+ $connected = get_transient( self::PF_API_CONNECT_STATUS );
63
+ if ( $connected && $connected['status'] == 1 ) {
64
+ $this->clear_connect_error();
65
+
66
+ return true;
67
+ } else if ( $connected && $connected['status'] == 0 ) { //try again in a minute
68
+ return false;
69
+ }
70
+ }
71
+
72
+ $client = $this->get_client();
73
+ $response = false;
74
+
75
+ //attempt to connect to printful to verify the API key
76
+ try {
77
+ $storeData = $client->get( 'store' );
78
+ if ( ! empty( $storeData ) && $storeData['type'] == 'woocommerce') {
79
+ $response = true;
80
+ $this->clear_connect_error();
81
+ set_transient( self::PF_API_CONNECT_STATUS, array( 'status' => 1 ) ); //no expiry
82
+ } elseif ( $storeData['type'] != 'woocommerce' ) {
83
+ $message = 'Invalid API key. This API key belongs to a ' . ucfirst( $storeData['type'] ) . ' store. Please copy the correct key from <a href="%s">Printful store settings</a> and enter it in the <a href="%s">Printful plugin settings</a>';
84
+ $settings_url = admin_url( 'admin.php?page=printful-dashboard&tab=settings' );
85
+ $printful_url = Printful_Base::get_printful_host() . 'dashboard/store';
86
+ $this->set_connect_error( sprintf( $message, $settings_url, $printful_url ) );
87
+ set_transient( self::PF_API_CONNECT_STATUS, array( 'status' => 0 ), MINUTE_IN_SECONDS ); //try again in 1 minute
88
+ }
89
+ } catch ( Exception $e ) {
90
+
91
+ if ( $e->getCode() == 401 ) {
92
+ $message = 'Invalid API key. Please ensure that your API key in <a href="%s">Printful plugin settings</a> matches the one in your <a href="%s">Printful store settings</a>.';
93
+ $settings_url = admin_url( 'admin.php?page=printful-dashboard&tab=settings' );
94
+ $printful_url = Printful_Base::get_printful_host() . 'dashboard/store';
95
+ $this->set_connect_error( sprintf( $message, $settings_url, $printful_url ) );
96
+ set_transient( self::PF_API_CONNECT_STATUS, array( 'status' => 0 ), MINUTE_IN_SECONDS ); //try again in 1 minute
97
+ } else {
98
+ $this->set_connect_error( 'Could not connect to Printful API. Please try again later. (Error ' . $e->getCode() . ': ' . $e->getMessage() . ')' );
99
+ }
100
+
101
+ //do nothing
102
+ set_transient( self::PF_API_CONNECT_STATUS, array( 'status' => 0 ), MINUTE_IN_SECONDS ); //try again in 1 minute
103
+ }
104
+
105
+ return $response;
106
+ }
107
+
108
+ /**
109
+ * Update connect error message
110
+ * @param string $error
111
+ */
112
+ public function set_connect_error($error = '') {
113
+ update_option( self::PF_CONNECT_ERROR, $error );
114
+ }
115
+
116
+ /**
117
+ * Get current connect error message
118
+ */
119
+ public function get_connect_error() {
120
+ return get_option( self::PF_CONNECT_ERROR, false );
121
+ }
122
+
123
+ /**
124
+ * Remove option used for storing current connect error
125
+ */
126
+ public function clear_connect_error() {
127
+ delete_option( self::PF_CONNECT_ERROR );
128
+ }
129
+
130
+ /**
131
+ * AJAX call endpoint for connect status check
132
+ * @throws PrintfulException
133
+ */
134
+ public static function ajax_force_check_connect_status() {
135
+ if ( Printful_Integration::instance()->is_connected( true ) ) {
136
+ die( 'OK' );
137
+ }
138
+
139
+ die( 'FAIL' );
140
+ }
141
+
142
+ /**
143
+ * Wrapper method for getting an option
144
+ * @param $name
145
+ * @param array $default
146
+ * @return bool
147
+ */
148
+ public function get_option( $name, $default = array() ) {
149
+ $options = get_option( 'woocommerce_printful_settings', $default );
150
+ if ( ! empty( $options[ $name ] ) ) {
151
+ return $options[ $name ];
152
+ }
153
+
154
+ return false;
155
+ }
156
+
157
+ /**
158
+ * Save the setting
159
+ * @param $settings
160
+ */
161
+ public function update_settings( $settings ) {
162
+ delete_transient( self::PF_API_CONNECT_STATUS ); //remove the successful API status since API key could have changed
163
+ update_option( 'woocommerce_printful_settings', $settings );
164
+ }
165
+ }
trunk/includes/class-printful-request-log.php ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Request_log {
5
+
6
+ const PF_USER_AGENT = 'Printful WooCommerce Integration';
7
+ const PF_OPTION_LAST_API_RESPONSE = 'printful_last_api_response';
8
+ const PF_OPTION_INCOMING_API_REQUEST_LOG = 'printful_incoming_api_request_log';
9
+ const PF_OPTION_OUTGOING_API_REQUEST_LOG = 'printful_outgoing_api_request_log';
10
+ const PF_INCOMING_API_ERRORS = 'printful-incoming-api-errors';
11
+ const PF_OUTGOING_API_ERRORS = 'printful-outgoing-api-errors';
12
+
13
+ public static function init() {
14
+ $printful_log = new self;
15
+ add_filter( 'woocommerce_api_serve_request', array( $printful_log, 'log_incoming_printful_api_requests' ), 10, 3 );
16
+ add_filter( 'printful_api_result', array( $printful_log, 'log_outgoing_printful_api_requests'), 10, 4 );
17
+ }
18
+
19
+ /**
20
+ * Log printful API errors and save the last 20 API responses
21
+ * @param $served
22
+ * @param $result
23
+ * @param $request
24
+ *
25
+ * @return mixed
26
+ */
27
+ public function log_incoming_printful_api_requests( $served, $result, $request ) {
28
+
29
+ if ( ! $this->isPrintfulApiRequest( $request ) ) {
30
+ return $served;
31
+ }
32
+
33
+ $last_api_response = get_option( self::PF_OPTION_LAST_API_RESPONSE, false );
34
+ $response_hash = md5( serialize( array( 'request' => $request, 'results' => $result ) ) );
35
+
36
+ if ( $last_api_response == $response_hash ) { //do not allow the same response to be logged twice
37
+ return $served;
38
+ }
39
+
40
+ //save full error to WC logs
41
+ if ( ! empty( $result['errors'] ) ) {
42
+ $this->save_to_wc_log( $request, $result, self::PF_INCOMING_API_ERRORS );
43
+ }
44
+
45
+ //save summary in database to be easily accessible for status page
46
+ $this->save_to_printful_log( $request->method . ' ' . $request->path, $result, $response_hash, self::PF_OPTION_INCOMING_API_REQUEST_LOG );
47
+
48
+ return $served; //we avoid changing the result
49
+ }
50
+
51
+
52
+ /**
53
+ * @param $result
54
+ * @param $method
55
+ * @param $url
56
+ * @param $request
57
+ * @return array|mixed|object
58
+ */
59
+ public function log_outgoing_printful_api_requests($result, $method, $url, $request) {
60
+
61
+ $original_result = $result;
62
+ $request['path'] = $url;
63
+ $params_set = null;
64
+ $code_success = null;
65
+
66
+ if ( ! is_wp_error( $result ) ) {
67
+ $result = json_decode( $result['body'], true );
68
+ $params_set = ! isset( $result['code'], $result['result'] );
69
+ $status = (int) $result['code'];
70
+ $code_success = ( $status < 200 || $status >= 300 );
71
+ }
72
+
73
+ //if the request contains error, log it
74
+ if ( is_wp_error($result) || $params_set || $code_success ) {
75
+ $this->save_to_wc_log( $request, $result, self::PF_OUTGOING_API_ERRORS );
76
+ }
77
+
78
+ $response_hash = md5( serialize( array( 'request' => $request, 'results' => $result ) ) );
79
+
80
+ //save summary in database to be easily accessible for status page
81
+ $this->save_to_printful_log( $method . ' ' . $url, $result, $response_hash, self::PF_OPTION_OUTGOING_API_REQUEST_LOG );
82
+
83
+ return $original_result; //don't change the result
84
+ }
85
+
86
+ /**
87
+ * Write Printful API request errors to log
88
+ *
89
+ * @param $request
90
+ * @param $result
91
+ * @param string $context
92
+ * @return bool
93
+ */
94
+ private function save_to_wc_log( $request, $result, $context ) {
95
+
96
+ if ( ! function_exists( 'wc_get_logger' ) ) {
97
+ return false;
98
+ }
99
+
100
+ $logger = wc_get_logger();
101
+ $context = array( 'source' => $context );
102
+ $log_item = array(
103
+ 'request' => (array) $request,
104
+ 'results' => (array) $result,
105
+ );
106
+ $logger->error( wc_print_r( $log_item, true ), $context );
107
+
108
+ return true;
109
+ }
110
+
111
+ /**
112
+ * Save 20 lasts requests in easily accessible location
113
+ *
114
+ * @param $request_title
115
+ * @param $result
116
+ * @param $response_hash
117
+ * @param $log
118
+ */
119
+ private function save_to_printful_log( $request_title, $result, $response_hash, $log ) {
120
+
121
+ $request_log = get_option( $log, array() );
122
+ if ( count( $request_log ) > 20 ) {
123
+ $request_log = array_slice( $request_log, 1, 19 ); //if there are more than 20, remove the first entry
124
+ }
125
+
126
+ $is_error = is_wp_error($result) || !empty( $result['errors'] );
127
+
128
+ $request_log[] = array(
129
+ 'date' => date( 'Y-m-d H:i:s' ),
130
+ 'request' => $request_title,
131
+ 'result' => ( $is_error ? 'ERROR' : 'OK' ),
132
+ );
133
+
134
+ update_option( $log, $request_log );
135
+ update_option( self::PF_OPTION_LAST_API_RESPONSE, $response_hash );
136
+ }
137
+
138
+ /**
139
+ * Check requests header for indications that this is a printful api request
140
+ * @param $request
141
+ * @return bool
142
+ */
143
+ private function isPrintfulApiRequest( $request ) {
144
+
145
+ if ( ! empty( $request->headers ) && ! empty( $request->headers['USER_AGENT'] ) && $request->headers['USER_AGENT'] == self::PF_USER_AGENT ) {
146
+ return true;
147
+ }
148
+
149
+ return false;
150
+ }
151
+ }
trunk/includes/class-printful-rest-api-controller.php ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * API class
5
+ */
6
+ class Printful_REST_API_Controller extends WC_REST_Controller
7
+ {
8
+ /**
9
+ * Endpoint namespace.
10
+ *
11
+ * @var string
12
+ */
13
+ protected $namespace = 'wc/v2';
14
+
15
+ /**
16
+ * Route base.
17
+ *
18
+ * @var string
19
+ */
20
+ protected $rest_base = 'printful';
21
+
22
+ /**
23
+ * Register the REST API routes.
24
+ */
25
+ public function register_routes() {
26
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/access', array(
27
+ array(
28
+ 'methods' => WP_REST_Server::EDITABLE,
29
+ 'callback' => array( $this, 'set_printful_access' ),
30
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
31
+ 'show_in_index' => false,
32
+ 'args' => array(
33
+ 'accessKey' => array(
34
+ 'required' => false,
35
+ 'type' => 'string',
36
+ 'description' => __( 'Printful access key', 'printful' ),
37
+ ),
38
+ 'storeId' => array(
39
+ 'required' => false,
40
+ 'type' => 'integer',
41
+ 'description' => __( 'Store Identifier', 'printful' ),
42
+ ),
43
+ ),
44
+ )
45
+ ) );
46
+
47
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/products/(?P<product_id>\d+)/size-chart', array(
48
+ array(
49
+ 'methods' => WP_REST_Server::EDITABLE,
50
+ 'callback' => array( $this, 'post_size_guide' ),
51
+ 'permission_callback' => array( $this, 'update_item_permissions_check' ),
52
+ 'show_in_index' => false,
53
+ 'args' => array(
54
+ 'product_id' => array(
55
+ 'description' => __( 'Unique identifier for the resource.', 'printful' ),
56
+ 'type' => 'integer',
57
+ ),
58
+ 'size_chart' => array(
59
+ 'required' => true,
60
+ 'type' => 'string',
61
+ 'description' => __( 'Printful size guide', 'printful' ),
62
+ )
63
+ )
64
+ )
65
+ ) );
66
+
67
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/products/(?P<product_id>\d+)/advanced-size-chart', array(
68
+ array(
69
+ 'methods' => WP_REST_Server::EDITABLE,
70
+ 'callback' => array( $this, 'post_size_guide' ),
71
+ 'permission_callback' => array( $this, 'update_item_permissions_check' ),
72
+ 'show_in_index' => false,
73
+ 'args' => array(
74
+ 'product_id' => array(
75
+ 'description' => __( 'Unique identifier for the resource.', 'printful' ),
76
+ 'type' => 'integer',
77
+ ),
78
+ 'size_chart' => array(
79
+ 'required' => true,
80
+ 'type' => 'object',
81
+ 'description' => __( 'Advanced Printful size guide', 'printful' ),
82
+ )
83
+ )
84
+ )
85
+ ) );
86
+
87
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/version', array(
88
+ array(
89
+ 'methods' => WP_REST_Server::READABLE,
90
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
91
+ 'callback' => array( $this, 'get_version' ),
92
+ 'show_in_index' => false,
93
+ )
94
+ ) );
95
+
96
+ register_rest_route( $this->namespace, '/' . $this->rest_base . '/store_data', array(
97
+ array(
98
+ 'methods' => WP_REST_Server::READABLE,
99
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
100
+ 'callback' => array( $this, 'get_store_data' ),
101
+ 'show_in_index' => true,
102
+ )
103
+ ) );
104
+ }
105
+
106
+ /**
107
+ * @param WP_REST_Request $request
108
+ * @return array
109
+ */
110
+ public static function set_printful_access( $request )
111
+ {
112
+ $error = false;
113
+
114
+ $options = get_option( 'woocommerce_printful_settings', array() );
115
+
116
+ $api_key = $request->get_param('accessKey');
117
+ $store_id = $request->get_param('storeId');
118
+ $store_id = intval( $store_id );
119
+
120
+ if ( ! is_string( $api_key ) || strlen( $api_key ) == 0 || $store_id == 0 ) {
121
+ $error = 'Failed to update access data';
122
+ }
123
+
124
+ $options['printful_key'] = $api_key;
125
+ $options['printful_store_id'] = $store_id;
126
+
127
+ Printful_Integration::instance()->update_settings( $options );
128
+
129
+ return array(
130
+ 'error' => $error,
131
+ );
132
+ }
133
+
134
+ /**
135
+ * Submit size guide
136
+ * @param array $data
137
+ * @return array|WP_Error
138
+ */
139
+ public static function post_size_guide( $data )
140
+ {
141
+ if ( empty( $data['size_chart'] ) ) {
142
+ return new WP_Error( 'printful_api_size_chart_empty', __( 'No size chart was provided', 'printful' ), array('status' => 400));
143
+ }
144
+
145
+ //product id is valid
146
+ $product_id = intval( $data['product_id'] );
147
+ if ( $product_id < 1 ) {
148
+ return new WP_Error( 'printful_api_product_not_found', __( 'The product ID is invalid', 'printful' ), array('status' => 400));
149
+ }
150
+
151
+ //product exists
152
+ /** @var WC_Product $product */
153
+ $product = wc_get_product( $product_id );
154
+ if ( empty( $product )) {
155
+ return new WP_Error( 'printful_api_product_not_found', __( 'The product is not found', 'printful' ), array('status' => 400));
156
+ }
157
+
158
+ //how about permissions?
159
+ $post_type = get_post_type_object( 'product' );
160
+ if ( ! current_user_can( $post_type->cap->edit_post, $product->get_id() ) ) {
161
+ return new WP_Error( 'printful_api_user_cannot_edit_product_size_chart', __( 'You do not have permission to edit the size chart', 'printful' ), array('status' => 401));
162
+ }
163
+
164
+ //lets do this
165
+ if( is_array( $data['size_chart']) ) {
166
+ // Advanced size guide
167
+ $payload = addslashes( json_encode( Printful_Size_Guide::process_size_guide_for_storage($data['size_chart'], $product_id ) ) );
168
+ $metaKey = 'pf_advanced_size_chart';
169
+ } else {
170
+ $payload = htmlspecialchars( $data['size_chart'] );
171
+ $metaKey = 'pf_size_chart';
172
+ }
173
+
174
+ update_post_meta( $product->get_id(), $metaKey, $payload );
175
+
176
+ return array(
177
+ 'product' => $product,
178
+ 'size_chart' => $data['size_chart'],
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Allow remotely get plugin version for debug purposes
184
+ */
185
+ public static function get_version() {
186
+ $error = false;
187
+
188
+ try {
189
+ $client = Printful_Integration::instance()->get_client();
190
+ $store_data = $client->get( 'store' );
191
+ } catch ( Exception $exception ) {
192
+ $error = $exception->getMessage();
193
+ }
194
+
195
+ $checklist = Printful_Admin_Status::get_checklist();
196
+ $checklist['overall_status'] = ( $checklist['overall_status'] ? 'OK' : 'FAIL' );
197
+
198
+ foreach ( $checklist['items'] as $checklist_key => $checklist_item ) {
199
+
200
+ if ( $checklist_item['status'] == Printful_Admin_Status::PF_STATUS_OK ) {
201
+ $checklist_item['status'] = 'OK';
202
+ } elseif ( $checklist_item['status'] == Printful_Admin_Status::PF_STATUS_WARNING ) {
203
+ $checklist_item['status'] = 'WARNING';
204
+ } elseif ( $checklist_item['status'] == Printful_Admin_Status::PF_STATUS_NOT_CONNECTED ) {
205
+ $checklist_item['status'] = 'NOT CONNECTED';
206
+ } else {
207
+ $checklist_item['status'] = 'FAIL';
208
+ }
209
+
210
+ $checklist['items'][ $checklist_key ] = $checklist_item;
211
+ }
212
+
213
+ return array(
214
+ 'version' => Printful_Base::VERSION,
215
+ 'store_id' => ! empty( $store_data['id'] ) ? $store_data['id'] : false,
216
+ 'error' => $error,
217
+ 'status_checklist' => $checklist,
218
+ );
219
+ }
220
+
221
+ /**
222
+ * Get necessary store data
223
+ * @return array
224
+ */
225
+ public static function get_store_data() {
226
+ return array(
227
+ 'website' => get_site_url(),
228
+ 'version' => WC()->version,
229
+ 'name' => get_bloginfo( 'title', 'display' )
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Check whether a given request has permission to read printful endpoints.
235
+ *
236
+ * @param WP_REST_Request $request Full details about the request.
237
+ * @return WP_Error|boolean
238
+ */
239
+ public function get_items_permissions_check( $request ) {
240
+ if ( ! wc_rest_check_user_permissions( 'read' ) ) {
241
+ return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
242
+ }
243
+
244
+ return true;
245
+ }
246
+
247
+ /**
248
+ * Check if a given request has access to update a product.
249
+ *
250
+ * @param WP_REST_Request $request Full details about the request.
251
+ * @return WP_Error|boolean
252
+ */
253
+ public function update_item_permissions_check( $request ) {
254
+ $params = $request->get_url_params();
255
+ $product = wc_get_product( (int) $params['product_id'] );
256
+
257
+ if ( empty( $product ) && ! wc_rest_check_post_permissions( 'product', 'edit', $product->get_id() ) ) {
258
+ return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) );
259
+ }
260
+
261
+ return true;
262
+ }
263
+ }
trunk/includes/class-printful-shipping.php ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Shipping extends WC_Shipping_Method
5
+ {
6
+ public $show_warnings = false;
7
+ public $calculate_tax = false;
8
+ public $override_defaults = true;
9
+ private $last_error = false;
10
+
11
+ const PRINTFUL_SHIPPING = 'printful_shipping';
12
+
13
+ //Store whether currently processed package contains Printful products (for WC<2.6)
14
+ private $printful_package = true;
15
+
16
+ public static function init() {
17
+ new self;
18
+ }
19
+
20
+ public function __construct() {
21
+
22
+ $this->id = 'printful_shipping';
23
+ $this->method_title = $this->title = 'Printful Shipping';
24
+ $this->method_description = 'Calculate live shipping rates based on actual Printful shipping costs.';
25
+
26
+ $this->init_form_fields();
27
+ $this->init_settings();
28
+
29
+ add_action( 'woocommerce_update_options_shipping_' . $this->id, array( &$this, 'process_admin_options' ) );
30
+
31
+ $this->enabled = $this->get_option( 'enabled' );
32
+ $this->show_warnings = $this->get_option( 'show_warnings' ) == 'yes';
33
+ $this->override_defaults = $this->get_option( 'override_defaults' ) == 'yes';
34
+
35
+ //Initialize shipping methods for specific package (or no package)
36
+ add_filter( 'woocommerce_load_shipping_methods', array( $this, 'woocommerce_load_shipping_methods' ), 10000 );
37
+
38
+ //Remove other shipping methods for Printful package on WC < 2.6
39
+ add_filter( 'woocommerce_shipping_methods', array( $this, 'woocommerce_shipping_methods' ), 10000 );
40
+
41
+ add_filter( 'woocommerce_cart_shipping_packages', array( $this, 'woocommerce_cart_shipping_packages' ), 10000 );
42
+ }
43
+
44
+ /**
45
+ * Init fields for Printful Shipping form
46
+ */
47
+ public function init_form_fields() {
48
+
49
+ $this->form_fields = array(
50
+ 'enabled' => array(
51
+ 'title' => __( 'Enable/Disable', 'woocommerce' ),
52
+ 'type' => 'checkbox',
53
+ 'label' => __( 'Enable this shipping method', 'woocommerce' ),
54
+ 'default' => 'no',
55
+ ),
56
+ 'override_defaults' => array(
57
+ 'title' => __( 'Disable Woocommerce rates', 'woocommerce' ),
58
+ 'type' => 'checkbox',
59
+ 'label' => __( 'Disable standard Woocommerce rates for products fulfilled by Printful', 'woocommerce' ),
60
+ 'default' => 'yes',
61
+ ),
62
+ 'show_warnings' => array(
63
+ 'title' => __( 'Show Printful warnings', 'woocommerce' ),
64
+ 'type' => 'checkbox',
65
+ 'label' => __( 'Display Printful status messages if rate API request fails', 'woocommerce' ),
66
+ 'default' => 'yes',
67
+ ),
68
+ );
69
+
70
+ if ( !Printful_Integration::instance()->get_option('printful_key') ) {
71
+ $this->form_fields['info'] = array(
72
+ 'type' => 'title',
73
+ 'description' => 'Please add Printful API key to the
74
+ <a href="' . admin_url( 'admin.php?page=printful-dashboard&tab=settings' ) . '">Printful Integration settings section</a>
75
+ to enable rate calculation.',
76
+ );
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Enable only Printful shipping method for Printful packages
82
+ * @param array $package
83
+ */
84
+ public function woocommerce_load_shipping_methods( $package = array() ) {
85
+
86
+ $this->printful_package = false;
87
+
88
+ if ( $package && ! empty( $package['printful'] ) ) {
89
+ if ( $this->enabled == 'yes' ) {
90
+ $this->printful_package = true;
91
+ if ( $this->override_defaults ) {
92
+ //Remove default methods if we process Printful package
93
+ WC()->shipping()->unregister_shipping_methods();
94
+ }
95
+ WC()->shipping()->register_shipping_method( $this );
96
+ }
97
+ } else if ( ! $package ) {
98
+ //Show Printful tab on Shipping rate settings
99
+ WC()->shipping()->register_shipping_method( $this );
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Remove non-Printful methods for Printful packages on WC < 2.6
105
+ * @param $methods
106
+ *
107
+ * @return array
108
+ */
109
+ public function woocommerce_shipping_methods( $methods ) {
110
+
111
+ if ( $this->override_defaults && $this->printful_package && version_compare( WC()->version, '2.6', '<' ) ) {
112
+ //For WC < 2.6 woocommerce_shipping_methods is executed after woocommerce_load_shipping_methods
113
+ //So we need to clean up unnecessary methods from there
114
+ return array();
115
+ }
116
+
117
+ return $methods;
118
+ }
119
+
120
+ /**
121
+ * Split Printul products to a separate package if there are any
122
+ * @param array $packages
123
+ *
124
+ * @return array
125
+ */
126
+ public function woocommerce_cart_shipping_packages( $packages = array() ) {
127
+ //Printful rates are turned off, do not split products
128
+ if ( $this->enabled !== 'yes' ) {
129
+ return $packages;
130
+ }
131
+
132
+ $return_packages = array();
133
+
134
+ foreach ( $packages as $package ) {
135
+ $ids = array();
136
+ foreach ( $package['contents'] as $key => $item ) {
137
+ $ids[ $key ] = $item['variation_id'] ? $item['variation_id'] : $item['product_id'];
138
+ }
139
+
140
+ $printful_ids = array();
141
+ if ( $ids ) {
142
+ asort( $ids );
143
+ $values = implode( ',', array_unique( $ids ) );
144
+ $key = 'printful_productids_' . md5( $values );
145
+ $printful_ids = get_transient( $key );
146
+ if ( ! is_array( $printful_ids ) ) {
147
+ $printful_ids = array();
148
+ try {
149
+ $client = Printful_Integration::instance()->get_client();
150
+ $status = $client->get( 'sync/variants', array(
151
+ 'external_ids' => $values,
152
+ ) );
153
+ if ( ! empty( $status['sync_variants'] ) ) {
154
+ foreach ( $status['sync_variants'] as $variant ) {
155
+ if ( $variant['synced'] ) {
156
+ $printful_ids[] = $variant['external_id'];
157
+ }
158
+ }
159
+ }
160
+ set_transient( $key, $printful_ids, 1800 );
161
+ } catch ( PrintfulException $e ) {
162
+ $this->set_error( $e );
163
+
164
+ //Failed to get Printful status, return default packages
165
+ return $packages;
166
+ }
167
+ }
168
+ }
169
+ $new_contents = array(
170
+ 'printful' => array(),
171
+ 'virtual' => array(),
172
+ 'woocommerce' => array(),
173
+ );
174
+
175
+ foreach ( $ids as $key => $external_id ) {
176
+ $item = $package['contents'][ $key ];
177
+ if ( in_array( $external_id, $printful_ids ) ) {
178
+ $new_contents['printful'][ $key ] = $item;
179
+ } else if ( $item['data']->is_virtual() || $item['data']->is_downloadable() ) {
180
+ $new_contents['virtual'][ $key ] = $item;
181
+ } else {
182
+ $new_contents['woocommerce'][ $key ] = $item;
183
+ }
184
+ }
185
+
186
+ //Put virtual products together with any other package
187
+ if ( $new_contents['virtual'] ) {
188
+ if ( $new_contents['printful'] && ! $new_contents['woocommerce'] ) {
189
+ $new_contents['printful'] += $new_contents['virtual'];
190
+ } else {
191
+ $new_contents['woocommerce'] += $new_contents['virtual'];
192
+ }
193
+ unset ( $new_contents['virtual'] );
194
+ }
195
+
196
+ foreach ( $new_contents as $key => $contents ) {
197
+ if ( $contents ) {
198
+ $new_package = $package;
199
+ $new_package['contents_cost'] = 0;
200
+ $new_package['contents'] = $contents;
201
+ foreach ( $contents as $item ) {
202
+ if ( $item['data']->needs_shipping() ) {
203
+ if ( isset( $item['line_total'] ) ) {
204
+ $new_package['contents_cost'] += $item['line_total'];
205
+ }
206
+ }
207
+ }
208
+ if ( $key == 'printful' ) {
209
+ $new_package['printful'] = true;
210
+ }
211
+ $return_packages[] = $new_package;
212
+ }
213
+ }
214
+ }
215
+
216
+ return $return_packages;
217
+ }
218
+
219
+ /**
220
+ * @param array $package
221
+ *
222
+ * @return bool
223
+ */
224
+ public function calculate_shipping( $package = array() ) {
225
+ $request = array(
226
+ 'recipient' => array(
227
+ 'address1' => $package['destination']['address'],
228
+ 'address2' => $package['destination']['address_2'],
229
+ 'city' => $package['destination']['city'],
230
+ 'state_code' => $package['destination']['state'],
231
+ 'country_code' => $package['destination']['country'],
232
+ 'zip' => $package['destination']['postcode'],
233
+ ),
234
+ 'items' => array(),
235
+ 'currency' => get_woocommerce_currency(),
236
+ 'locale' => get_locale()
237
+ );
238
+
239
+
240
+ if ( $request['recipient']['country_code'] == 'US' &&
241
+ ( empty( $request['recipient']['state_code'] ) )
242
+ ) {
243
+ return false;
244
+ }
245
+
246
+ foreach ( $package['contents'] as $item ) {
247
+ if ( ! empty( $item['data'] ) && ( $item['data']->is_virtual() || $item['data']->is_downloadable() ) ) {
248
+ continue;
249
+ }
250
+ $request['items'] [] = array(
251
+ 'external_variant_id' => $item['variation_id'] ? $item['variation_id'] : $item['product_id'],
252
+ 'quantity' => $item['quantity'],
253
+ 'value' => $item['line_total'] / $item['quantity'],
254
+ );
255
+ }
256
+
257
+ if ( ! $request['items'] ) {
258
+ return false;
259
+ }
260
+
261
+ try {
262
+ $client = Printful_Integration::instance()->get_client();
263
+ } catch ( PrintfulException $e ) {
264
+ $this->set_error( $e );
265
+
266
+ return false;
267
+ }
268
+
269
+ try {
270
+ $key = 'printful_rates_' . md5( json_encode( $request ) );
271
+ $response = get_transient( $key );
272
+ if ( $response === false ) {
273
+ $response = $client->post( 'shipping/rates', $request, array(
274
+ 'expedited' => true,
275
+ 'is_billing_phone_number_mandatory' => $this->isBillingPhoneNumberRequired(),
276
+ ) );
277
+ //Cache locally, since WC < 2.6 had problems with caching rates form multiple packages internally
278
+ set_transient( $key, $response, 1800 );
279
+ }
280
+
281
+ foreach ( $response as $rate ) {
282
+ $rateData = array(
283
+ 'id' => $this->id . '_' . $rate['id'],
284
+ 'label' => $rate['name'],
285
+ 'cost' => $rate['rate'],
286
+ 'calc_tax' => 'per_order',
287
+ );
288
+
289
+ // Before 3.4.0 rate could be passed as ID, after it's set as method_id which refers to class ID
290
+ if ( version_compare( WC()->version, '3.4.0', '>=' ) ) {
291
+ $this->id = self::PRINTFUL_SHIPPING . '_' . $rate['id'];
292
+ }
293
+
294
+ $this->add_rate( $rateData );
295
+ // Reset class ID after adding rate so ID name does not stack as huge string in foreach
296
+ $this->id = self::PRINTFUL_SHIPPING;
297
+ }
298
+ } catch ( PrintfulException $e ) {
299
+ $this->set_error( $e );
300
+ return false;
301
+ }
302
+
303
+ return false;
304
+ }
305
+
306
+ /**
307
+ * @param $error
308
+ */
309
+ private function set_error( $error ) {
310
+ if ( $this->show_warnings ) {
311
+ $this->last_error = $error;
312
+ add_filter( 'woocommerce_cart_no_shipping_available_html', array( $this, 'show_error' ) );
313
+ add_filter( 'woocommerce_no_shipping_available_html', array( $this, 'show_error' ) );
314
+ }
315
+ }
316
+
317
+ /**
318
+ * @param $data
319
+ *
320
+ * @return string
321
+ */
322
+ public function show_error( $data ) {
323
+ $error = $this->last_error;
324
+ $message = $error->getMessage();
325
+
326
+ if ( $error instanceof PrintfulApiException && $error->getCode() == 401 ) {
327
+ $message = 'Invalid API key';
328
+ }
329
+
330
+ return '<p>ERROR: ' . htmlspecialchars( $message ) . '</p>';
331
+ }
332
+
333
+ private function isBillingPhoneNumberRequired()
334
+ {
335
+ return get_option('woocommerce_checkout_phone_field', 'required') === 'required';
336
+ }
337
+ }
trunk/includes/class-printful-size-chart-tab.php ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Size_Chart_Tab {
5
+
6
+ /**
7
+ * Is meta boxes saved once?
8
+ *
9
+ * @var boolean
10
+ */
11
+ private static $saved_meta_boxes = false;
12
+
13
+ /**
14
+ * Printful_Size_Chart_Tab constructor.
15
+ */
16
+ public static function init() {
17
+ $size_chart = new self;
18
+ add_filter( 'woocommerce_product_tabs', array( $size_chart, 'init_size_chart_tab' ) );
19
+ add_action( 'add_meta_boxes', array( $size_chart, 'init_metabox' ) );
20
+ add_action( 'save_post', array( $size_chart, 'save_size_chart' ), 1, 2 );
21
+ }
22
+
23
+ /**
24
+ * Initialize meta boxes
25
+ */
26
+ public function init_metabox() {
27
+ global $post;
28
+ // If product has advanced size chart we don't show basic size chart metabox at all
29
+ if (Printful_Size_Guide::get_size_guide_for_product($post)) {
30
+ return;
31
+ }
32
+ add_meta_box( 'pf_size_chart', __( 'Size chart', 'printful' ), array( $this, 'size_chart_metabox' ), 'product', 'normal' );
33
+ }
34
+
35
+ /**
36
+ * @param $tabs
37
+ *
38
+ * @return mixed
39
+ */
40
+ public function init_size_chart_tab( $tabs ) {
41
+ if ( strlen( $this->get_size_chart_content() ) > 0 ) {
42
+ $tabs['size_chart'] = array(
43
+ 'title' => __( 'Size Chart', 'printful' ),
44
+ 'priority' => 50,
45
+ 'callback' => array( $this, 'size_chart_tab_content' ),
46
+ );
47
+ }
48
+
49
+ return $tabs;
50
+ }
51
+
52
+ /**
53
+ * Display the size chart content
54
+ */
55
+ public function size_chart_tab_content() {
56
+ echo '<h2>' . esc_html__( 'Size Chart', 'printful' ) . '</h2>';
57
+ echo $this->get_size_chart_content();
58
+ }
59
+
60
+ /**
61
+ * @return mixed
62
+ */
63
+ public function get_size_chart_content() {
64
+ global $post;
65
+
66
+ return htmlspecialchars_decode(get_post_meta( $post->ID, 'pf_size_chart', true ));
67
+ }
68
+
69
+ /**
70
+ * @param $meta_id
71
+ */
72
+ public function size_chart_metabox( $meta_id ) {
73
+ $settings = array(
74
+ 'textarea_name' => 'pf_size_chart',
75
+ 'tinymce' => array(
76
+ 'theme_advanced_buttons1' => 'bold,italic,strikethrough,separator,bullist,numlist,separator,blockquote,separator,justifyleft,justifycenter,justifyright,separator,link,unlink,separator,undo,redo,separator',
77
+ 'theme_advanced_buttons2' => '',
78
+ ),
79
+ 'editor_css' => '<style>#wp-pf_size_chart-editor-container .wp-editor-area{height:175px; width:100%;} .wp-editor-area{height:175px; width:100%;}</style>',
80
+ );
81
+
82
+ $content = get_post_meta( $meta_id->ID, 'pf_size_chart', true );
83
+
84
+ wp_editor( htmlspecialchars_decode( $content ), 'pf_size_chart_editor', apply_filters( 'woocommerce_product_short_description_editor_settings', $settings ) );
85
+ }
86
+
87
+ /**
88
+ * @param $post_id
89
+ * @param $post
90
+ */
91
+ public function save_size_chart( $post_id, $post ) {
92
+
93
+ // $post_id and $post are required
94
+ if ( empty( $post_id ) || empty( $post ) || self::$saved_meta_boxes ) {
95
+ return;
96
+ }
97
+
98
+ // Dont' save meta boxes for revisions or autosaves
99
+ if ( defined( 'DOING_AUTOSAVE' ) || is_int( wp_is_post_revision( $post ) ) || is_int( wp_is_post_autosave( $post ) ) ) {
100
+ return;
101
+ }
102
+
103
+ // Check the nonce
104
+ if ( empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( $_POST['woocommerce_meta_nonce'], 'woocommerce_save_data' ) ) {
105
+ return;
106
+ }
107
+
108
+ // Check the post being saved == the $post_id to prevent triggering this call for other save_post events
109
+ if ( empty( $_POST['post_ID'] ) || $_POST['post_ID'] != $post_id ) {
110
+ return;
111
+ }
112
+
113
+ // Check user has permission to edit
114
+ if ( ! current_user_can( 'edit_post', $post_id ) ) {
115
+ return;
116
+ }
117
+
118
+ // Check the post type
119
+ if ( $post->post_type != 'product' ) {
120
+ return;
121
+ }
122
+
123
+ // We need this save event to run once to avoid potential endless loops.
124
+ self::$saved_meta_boxes = true;
125
+
126
+ //save
127
+ if (!empty($_POST['pf_size_chart'])) {
128
+ update_post_meta($post_id, 'pf_size_chart', htmlspecialchars($_POST['pf_size_chart']));
129
+ }
130
+ }
131
+ }
trunk/includes/class-printful-size-guide.php ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+
4
+ class Printful_Size_Guide {
5
+ /**
6
+ * Bump this everytime you make changes to size guide CSS file
7
+ */
8
+ const CSS_VERSION = '1';
9
+
10
+ public static function init() {
11
+ $sizeGuide = new self();
12
+
13
+ $sizeGuide->hook_actions();
14
+ }
15
+
16
+ public function hook_actions() {
17
+ add_action( 'wp_enqueue_scripts', array( $this, 'load_size_guide_scripts' ) );
18
+ }
19
+
20
+ public function load_size_guide_scripts() {
21
+ global $post;
22
+
23
+ $sizeGuideData = self::get_size_guide_for_product( $post );
24
+ if ( ! $sizeGuideData ) {
25
+ return;
26
+ }
27
+
28
+ $handle = 'printful-product-size-guide';
29
+ wp_enqueue_script( $handle, plugins_url( '../assets/js/product-size-guide.js', __FILE__ ) );
30
+ wp_localize_script(
31
+ $handle,
32
+ 'pfGlobal',
33
+ array(
34
+ 'sg_modal_title' => Printful_Integration::instance()
35
+ ->get_option( 'pfsg_modal_title', Printful_Admin_Settings::DEFAULT_SIZE_GUIDE_MODAL_TITLE ),
36
+ 'sg_modal_text_color' => Printful_Integration::instance()
37
+ ->get_option( 'pfsg_modal_text_color', Printful_Admin_Settings::DEFAULT_SIZE_GUIDE_MODAL_TEXT_COLOR ),
38
+ 'sg_modal_background_color' => Printful_Integration::instance()
39
+ ->get_option(
40
+ 'pfsg_modal_background_color',
41
+ Printful_Admin_Settings::DEFAULT_SIZE_GUIDE_MODAL_BACKGROUND_COLOR
42
+ ),
43
+ 'sg_tab_background_color' => Printful_Integration::instance()
44
+ ->get_option(
45
+ 'pfsg_tab_background_color',
46
+ Printful_Admin_Settings::DEFAULT_SIZE_GUIDE_TAB_BACKGROUND_COLOR
47
+ ),
48
+ 'sg_active_tab_background_color' => Printful_Integration::instance()
49
+ ->get_option(
50
+ 'pfsg_active_tab_background_color',
51
+ Printful_Admin_Settings::DEFAULT_SIZE_GUIDE_ACTIVE_TAB_BACKGROUND_COLOR
52
+ ),
53
+ 'sg_primary_unit' => Printful_Integration::instance()
54
+ ->get_option( 'pfsg_primary_unit', Printful_Admin_Settings::DEFAULT_SIZE_GUIDE_UNIT ),
55
+ 'sg_data_raw' => json_encode( $sizeGuideData ),
56
+ 'sg_tab_title_person' => __( 'Measure yourself', 'printful' ),
57
+ 'sg_tab_title_product' => __( 'Product measurements', 'printful' ),
58
+ 'sg_table_header_size' => __( 'Size', 'printful' ),
59
+ 'sg_unit_translations' => json_encode( [
60
+ 'inch' => __( 'Inches', 'printful' ),
61
+ 'centimeter' => __( 'Centimeters', 'printful' ),
62
+ ] )
63
+ )
64
+ );
65
+ wp_register_style( $handle, plugins_url( '../assets/css/size-guide.css', __FILE__ ), [], self::CSS_VERSION );
66
+ wp_enqueue_style( $handle );
67
+ }
68
+
69
+ /**
70
+ * @param WP_Post $product
71
+ *
72
+ * @return array|null
73
+ */
74
+ public static function get_size_guide_for_product( $product = null ) {
75
+ if ( ! $product || ! get_post_meta( $product->ID, 'pf_advanced_size_chart', true ) ) {
76
+ return null;
77
+ }
78
+
79
+ // Size guide data should be a valid JSON
80
+ $size_guide_data = json_decode( get_post_meta( $product->ID, 'pf_advanced_size_chart', true ), true );
81
+
82
+ if ( ! self::is_size_guide_valid( $size_guide_data ) ) {
83
+ return null;
84
+ }
85
+
86
+ // URLs to size guide images
87
+ if ( isset( $size_guide_data['modelMeasurements']['imageId'] ) ) {
88
+ $size_guide_data['modelMeasurements']['imageUrl'] = self::get_attached_image_src( $size_guide_data['modelMeasurements']['imageId'] );
89
+ }
90
+
91
+ if ( isset( $size_guide_data['productMeasurements']['imageId'] ) ) {
92
+ $size_guide_data['productMeasurements']['imageUrl'] = self::get_attached_image_src( $size_guide_data['productMeasurements']['imageId'] );
93
+ }
94
+
95
+ return $size_guide_data;
96
+ }
97
+
98
+ /**
99
+ * @param array $size_guide
100
+ *
101
+ * @return bool
102
+ */
103
+ public static function is_size_guide_valid( $size_guide = null ) {
104
+ if ( empty( $size_guide ) || ! is_array( $size_guide ) ) {
105
+ return false;
106
+ }
107
+
108
+ // Size guide should have at least model or product measurement data
109
+ if ( ( empty( $size_guide['modelMeasurements'] ) && empty( $size_guide['productMeasurements'] ) )
110
+ || empty( $size_guide['availableSizes'] ) ) {
111
+ return false;
112
+ }
113
+
114
+ // Validate size guide rows
115
+ $rows = [];
116
+ if ( ! empty( $size_guide['modelMeasurements']['sizeTableRows'] ) ) {
117
+ $rows += $size_guide['modelMeasurements']['sizeTableRows'];
118
+ }
119
+
120
+ if ( ! empty( $size_guide['productMeasurements']['sizeTableRows'] ) ) {
121
+ $rows += $size_guide['productMeasurements']['sizeTableRows'];
122
+ }
123
+
124
+ foreach ( $rows as $row ) {
125
+ if ( empty( $row['unit'] ) || empty( $row['sizes'] ) ) {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * @param int $attachment_id
135
+ *
136
+ * @return string|null
137
+ */
138
+ public static function get_attached_image_src( $attachment_id ) {
139
+ $attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
140
+
141
+ if ( ! is_array( $attachment ) ) {
142
+ return null;
143
+ }
144
+
145
+ return current( $attachment );
146
+ }
147
+
148
+ /**
149
+ * Creates file from given url and stores as post attachment
150
+ *
151
+ * @param string $url
152
+ * @param string $file_name
153
+ *
154
+ * @return int|WP_Error
155
+ */
156
+ public static function save_image( $url, $file_name ) {
157
+ if ( ! $url ) {
158
+ return null;
159
+ }
160
+
161
+ require_once ABSPATH . 'wp-admin/includes/file.php';
162
+
163
+ // Download size guide img to temp file
164
+ $temp_file_name = download_url( $url, 20 );
165
+ if ( is_wp_error( $temp_file_name ) ) {
166
+ return $temp_file_name;
167
+ }
168
+
169
+ // Validate image readability
170
+ require_once ABSPATH . 'wp-admin/includes/image.php';
171
+ if ( ! file_is_displayable_image( $temp_file_name ) ) {
172
+ @unlink( $temp_file_name );
173
+
174
+ return new WP_Error( 'pf_size_guide_img', 'Size guide attachment is not a valid image' );
175
+ }
176
+
177
+ $file_data = array(
178
+ 'name' => $file_name,
179
+ 'tmp_name' => $temp_file_name,
180
+ );
181
+
182
+ // Handle side load and attach to a post
183
+ require_once ABSPATH . 'wp-admin/includes/media.php';
184
+ $id = media_handle_sideload( $file_data );
185
+
186
+ @unlink( $temp_file_name );
187
+
188
+ if ( is_wp_error( $id ) ) {
189
+ return $id;
190
+ }
191
+
192
+ return $id;
193
+ }
194
+
195
+ /**
196
+ * @param array $size_guide_data
197
+ * @param int $product_id
198
+ *
199
+ * @return array
200
+ */
201
+ public static function process_size_guide_for_storage( $size_guide_data, $product_id ) {
202
+ $modelImageUrl = ! empty( $size_guide_data['modelMeasurements']['imageUrl'] )
203
+ ? $size_guide_data['modelMeasurements']['imageUrl'] : null;
204
+
205
+ $productImageUrl = ! empty( $size_guide_data['productMeasurements']['imageUrl'] )
206
+ ? $size_guide_data['productMeasurements']['imageUrl'] : null;
207
+
208
+ // Remove original URLs
209
+ unset( $size_guide_data['modelMeasurements']['imageUrl'] );
210
+ unset( $size_guide_data['productMeasurements']['imageUrl'] );
211
+
212
+ $modelImageId = self::save_image( $modelImageUrl, $product_id . '_model_size_guide.png' );
213
+ $productImageId = self::save_image( $productImageUrl, $product_id . '_product_size_guide.png' );
214
+
215
+ // Link size guide image attachments to size guide
216
+ $size_guide_data['modelMeasurements']['imageId'] = ! is_wp_error( $modelImageId ) ? $modelImageId : null;
217
+ $size_guide_data['productMeasurements']['imageId'] = ! is_wp_error( $productImageId ) ? $productImageId : null;
218
+
219
+ return $size_guide_data;
220
+ }
221
+ }
trunk/includes/class-printful-taxes.php ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Taxes {
5
+
6
+ /**
7
+ * Initialize the values, hooks and actions
8
+ */
9
+ public static function init() {
10
+ if ( Printful_Integration::instance()->get_option( 'calculate_tax' ) == 'yes' ) {
11
+ //Update tax options if taxes are enabled
12
+ if ( get_option( 'woocommerce_calc_taxes' ) != 'yes' ) {
13
+ update_option( 'woocommerce_calc_taxes', 'yes' );
14
+ }
15
+ if ( get_option( 'woocommerce_tax_based_on' ) != 'shipping' ) {
16
+ update_option( 'woocommerce_tax_based_on', 'shipping' );
17
+ }
18
+
19
+ //Override tax rates calculated by Woocommerce
20
+ $taxes = new self;
21
+ add_filter( 'woocommerce_matched_tax_rates', array( $taxes, 'calculate_tax' ), 10, 6 );
22
+ }
23
+ }
24
+
25
+ /**
26
+ * @param $matched_tax_rates
27
+ * @param $country
28
+ * @param $state
29
+ * @param $postcode
30
+ * @param $city
31
+ * @param $tax_class
32
+ *
33
+ * @return mixed
34
+ */
35
+ public function calculate_tax( $matched_tax_rates, $country, $state, $postcode, $city, $tax_class ) {
36
+ //if a tax rate is already matched, avoid adding extra one
37
+ if ( ! empty( $matched_tax_rates ) ) {
38
+ return $matched_tax_rates;
39
+ }
40
+
41
+ $countries = $this->get_tax_countries();
42
+ if ( isset( $countries[ $country ][ $state ] ) && !empty($postcode) ) { //only make the request if country, state and zip are set
43
+ $key = 'printful_tax_rate_' . $country . '-' . $state . '-' . $city . '-' . $postcode;
44
+ $rate = get_transient( $key );
45
+ $response = null;
46
+
47
+ if ( $rate === false ) {
48
+ try {
49
+ $client = Printful_Integration::instance()->get_client();
50
+ $response = $client->post( 'tax/rates', array(
51
+ 'recipient' => array(
52
+ 'country_code' => $country,
53
+ 'state_code' => $state,
54
+ 'city' => $city,
55
+ 'zip' => $postcode,
56
+ ),
57
+ ) );
58
+ } catch ( Exception $e ) {
59
+ }
60
+
61
+ if ( isset( $response['rate'] ) ) {
62
+ $rate = $response;
63
+ } else {
64
+ $rate = array(
65
+ 'required' => false,
66
+ 'rate' => 0,
67
+ 'shipping_taxable' => false,
68
+ );
69
+ }
70
+ set_transient( $key, $rate, 1800 );
71
+ }
72
+
73
+ if ( $rate['required'] ) {
74
+ $rate_item = array(
75
+ 'rate' => $rate['rate'] * 100,
76
+ 'label' => 'Sales Tax',
77
+ 'shipping' => $rate['shipping_taxable'] ? 'yes' : 'no',
78
+ 'compound' => 'no',
79
+ );
80
+
81
+ if ( $this->isRateUnique( $rate_item, $matched_tax_rates ) ) {
82
+ $id = $this->get_printful_rate_id( $country, $state, $rate['shipping_taxable'] );
83
+ $matched_tax_rates[ $id ] = $rate_item;
84
+ }
85
+ }
86
+ }
87
+
88
+ return $matched_tax_rates;
89
+ }
90
+
91
+ /**
92
+ * Checks if a equal tax rate is not already set
93
+ *
94
+ * @param $rate
95
+ * @param $matched_tax_rates
96
+ *
97
+ * @return bool
98
+ */
99
+ private function isRateUnique( $rate, $matched_tax_rates ) {
100
+ if ( empty( $matched_tax_rates ) ) {
101
+ return true;
102
+ }
103
+
104
+ foreach ( $matched_tax_rates as $mr ) {
105
+ if ( floatval( $mr['rate'] ) == floatval( $rate['rate'] ) && $mr['shipping'] == $rate['shipping'] ) {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ return true;
111
+ }
112
+
113
+ /**
114
+ * Gets list of countries and states where Printful needs to calculate sales tax
115
+ * @return array|mixed
116
+ */
117
+ private function get_tax_countries() {
118
+ $countries = get_transient( 'printful_tax_countries' );
119
+
120
+ if ( ! $countries ) {
121
+ $countries = array();
122
+
123
+ try {
124
+ $client = Printful_Integration::instance()->get_client();
125
+ $list = $client->get( 'tax/countries' );
126
+
127
+ foreach ( $list as $country ) {
128
+ foreach ( $country['states'] as $state ) {
129
+ $countries[ $country['code'] ][ $state['code'] ] = 1;
130
+ }
131
+ }
132
+
133
+ if ( ! empty( $countries ) ) {
134
+ set_transient( 'printful_tax_countries', $countries, 6 * 3600 );
135
+ }
136
+ } catch ( Exception $e ) {
137
+ //Default to CA if can't get the actual state list
138
+ return array( 'US' => array( 'CA' => 1 ) );
139
+ }
140
+ }
141
+
142
+ return $countries;
143
+ }
144
+
145
+ /**
146
+ * Creates dummy tax rate ID to display Printful tax rates in the cart summary.
147
+ *
148
+ * @param $cc
149
+ * @param $state
150
+ * @param bool $includeShipping
151
+ *
152
+ * @return int|null|string
153
+ */
154
+ private function get_printful_rate_id( $cc, $state, $includeShipping = false ) {
155
+ global $wpdb;
156
+
157
+ $includeShipping = (int) $includeShipping;
158
+
159
+ $states = WC()->countries->get_states( $cc );
160
+ $tax_title = ( isset( $states[ $state ] ) ? $states[ $state ] . ' ' : '' ) . __( 'Sales Tax', 'printful' );
161
+ $id = $wpdb->get_var(
162
+ $wpdb->prepare( "SELECT tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class='printful'
163
+ and tax_rate_country = %s AND tax_rate_state = %s AND tax_rate_shipping = %s LIMIT 1",
164
+ $cc,
165
+ $state,
166
+ $includeShipping
167
+ ) );
168
+ if ( empty( $id ) ) {
169
+ $wpdb->insert(
170
+ $wpdb->prefix . "woocommerce_tax_rates",
171
+ array(
172
+ 'tax_rate_country' => $cc,
173
+ 'tax_rate_state' => $state,
174
+ 'tax_rate' => 0,
175
+ 'tax_rate_name' => $tax_title,
176
+ 'tax_rate_priority' => 1,
177
+ 'tax_rate_compound' => 0,
178
+ 'tax_rate_shipping' => $includeShipping,
179
+ 'tax_rate_class' => 'printful',
180
+ )
181
+ );
182
+ $id = $wpdb->insert_id;
183
+ }
184
+
185
+ return $id;
186
+ }
187
+
188
+
189
+ }
trunk/includes/class-printful-template.php ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) exit;
3
+
4
+ class Printful_Template {
5
+
6
+ /**
7
+ * Initialize the values, hooks and actions
8
+ */
9
+ public static function init()
10
+ {
11
+ $template = new self;
12
+
13
+ $template->hook_templates();
14
+ }
15
+
16
+ /**
17
+ * Hook custom modifications in template files
18
+ */
19
+ public function hook_templates()
20
+ {
21
+ // hook templates, 29 indicates position, right before variation selection
22
+ add_action( 'woocommerce_after_add_to_cart_button', array( $this, 'printful_template_customize_button' ), 20 );
23
+ // add a hidden input field
24
+ add_action( 'woocommerce_before_add_to_cart_button', array( $this, 'printful_customizer_hash_field' ), 11 );
25
+ // Hook size guide button
26
+ add_action( 'woocommerce_before_single_variation', array( $this, 'printful_size_guide_button' ), 20 );
27
+ }
28
+
29
+ /**
30
+ * Hook callback for personalization button within product page
31
+ */
32
+ public static function printful_template_customize_button()
33
+ {
34
+ global $post;
35
+
36
+ if ( $post && get_post_meta( $post->ID, 'pf_customizable', true ) ) {
37
+ // load template for personalization button
38
+ Printful_Admin::load_template( 'personalize-button', array(
39
+ 'site_url' => get_site_url(),
40
+ 'pfc_button_color' => Printful_Integration::instance()->get_option( 'pfc_button_color' ) ?: Printful_Admin_Settings::DEFAULT_PERSONALIZE_BUTTON_COLOR,
41
+ 'pfc_button_text' => Printful_Integration::instance()->get_option( 'pfc_button_text' ) ?: Printful_Admin_Settings::DEFAULT_PERSONALIZE_BUTTON_TEXT,
42
+ 'pfc_modal_title' => Printful_Integration::instance()->get_option( 'pfc_modal_title' ) ?: Printful_Admin_Settings::DEFAULT_PERSONALIZE_MODAL_TITLE,
43
+ ) );
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Hook callback for size guide button render within product page
49
+ */
50
+ public static function printful_size_guide_button()
51
+ {
52
+ global $post;
53
+
54
+ $sizeGuideData = Printful_Size_Guide::get_size_guide_for_product($post);
55
+ if (!$sizeGuideData) {
56
+ return;
57
+ }
58
+
59
+ // Load size guide button template
60
+ Printful_Admin::load_template( 'size-guide-button', array(
61
+ 'size_guide_button_color' => Printful_Integration::instance()->get_option( 'pfsg_button_color' ) ?: Printful_Admin_Settings::DEFAULT_SIZE_GUIDE_BUTTON_COLOR,
62
+ 'size_guide_button_text' => Printful_Integration::instance()->get_option( 'pfsg_button_text' ) ?: Printful_Admin_Settings::DEFAULT_SIZE_GUIDE_BUTTON_TEXT,
63
+ ) );
64
+ }
65
+
66
+ /**
67
+ * Add hidden customizer hash ID field to form
68
+ */
69
+ public static function printful_customizer_hash_field()
70
+ {
71
+ global $post;
72
+
73
+ if ( $post && get_post_meta( $post->ID, 'pf_customizable', true ) ) {
74
+ Printful_Admin::load_template('customizer-hidden-input');
75
+ }
76
+ }
77
+ }
trunk/includes/templates/ajax-loader.php ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <div id="loader-block-<?php echo esc_attr($action); ?>">
2
+ <div class="block-loader loader-wrap">
3
+ <img src="<?php echo esc_url( admin_url( 'images/spinner-2x.gif' ) ) ?>" class="loader" width="20px" height="20px" alt="loader"/>
4
+ <span class="message"><?php echo esc_html($message); ?></span>
5
+ </div>
6
+ </div>
7
+ <script type="text/javascript">
8
+ jQuery(document).ready(function () {
9
+ Printful_Block_Loader.load('<?php echo esc_url(admin_url( 'admin-ajax.php?action=' . $action )); ?>', 'loader-block-<?php echo esc_attr($action); ?>');
10
+ });
11
+ </script>
trunk/includes/templates/connect.php ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="printful-connect">
2
+
3
+ <div class="printful-connect-inner">
4
+
5
+ <h1><?php esc_html_e('Connect to Printful', 'printful'); ?></h1>
6
+
7
+ <img src=" <?php echo esc_url(Printful_Base::get_asset_url() . 'images/connect.svg'); ?>" class="connect-image" alt="connect to printful">
8
+
9
+ <?php
10
+ if ( ! empty( $issues ) ) {
11
+ ?>
12
+ <p><?php esc_html_e('To connect your store to Printful, fix the following errors:', 'printful'); ?></p>
13
+ <div class="printful-notice">
14
+ <ul>
15
+ <?php
16
+ foreach ( $issues as $issue ) {
17
+ echo '<li>' . wp_kses_post( $issue ) . '</li>';
18
+ }
19
+ ?>
20
+ </ul>
21
+ </div>
22
+ <?php
23
+ $url = '#';
24
+ } else {
25
+ ?><p class="connect-description"><?php esc_html_e('You\'re almost done! Just 2 more steps to have your WooCommerce store connected to Printful for automatic order fulfillment.', 'printful'); ?></p><?php
26
+ $url = Printful_Base::get_printful_host() . 'dashboard/woocommerce/plugin-connect?website=' . urlencode( trailingslashit( get_home_url() ) ) . '&key=' . urlencode( $consumer_key ) . '&returnUrl=' . urlencode( get_admin_url( null,'admin.php?page=' . Printful_Admin::MENU_SLUG_DASHBOARD ) );
27
+ }
28
+
29
+ echo '<a href="' . esc_url($url) . '" class="button button-primary printful-connect-button ' . ( ! empty( $issues ) ? 'disabled' : '' ) . '" target="_blank">' . esc_html__('Connect', 'printful') . '</a>';
30
+ ?>
31
+
32
+ <img src="<?php echo esc_url( admin_url( 'images/spinner-2x.gif' ) ) ?>" class="loader hidden" width="20px" height="20px" alt="loader"/>
33
+
34
+ <script type="text/javascript">
35
+ jQuery(document).ready(function () {
36
+ Printful_Connect.init('<?php echo esc_url( admin_url( 'admin-ajax.php?action=ajax_force_check_connect_status' ) ); ?>');
37
+ });
38
+ </script>
39
+ </div>
40
+ </div>
trunk/includes/templates/customizer-hidden-input.php ADDED
@@ -0,0 +1 @@
 
1
+ <input type="hidden" id="pfc_hash" name="pfc_hash" value="">
trunk/includes/templates/error.php ADDED
@@ -0,0 +1 @@
 
1
+ <p class="printful-error"><b><?php esc_html_e('Error:', 'printful'); ?></b> <?php echo wp_kses_post($error); ?></p>
trunk/includes/templates/footer.php ADDED
@@ -0,0 +1 @@
 
1
+ </div>
trunk/includes/templates/header.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="wrap">
2
+
3
+ <?php
4
+ $base_url = '?page=printful-dashboard';
5
+ ?>
6
+
7
+ <h2 class="nav-tab-wrapper printful-tabs">
8
+ <?php foreach ($tabs as $tab) : ?>
9
+ <?php
10
+ $active = '';
11
+ if ( ! empty( $_GET['tab'] ) && $_GET['tab'] == $tab['tab_url'] ) {
12
+ $active = 'nav-tab-active';
13
+ }
14
+ if ( empty( $_GET['tab'] ) && $tab['tab_url'] == '' ) {
15
+ $active = 'nav-tab-active';
16
+ }
17
+ ?>
18
+ <a href="<?php echo esc_url($base_url . ($tab['tab_url'] ? '&tab=' . $tab['tab_url'] : '') ); ?>" class="nav-tab <?php echo esc_attr($active);?>"><?php echo esc_html($tab['name']); ?></a>
19
+ <?php endforeach; ?>
20
+ </h2>
trunk/includes/templates/inline-script.php ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ <script type="text/javascript">
2
+ <?php echo esc_js( $script ); ?>
3
+ </script>
trunk/includes/templates/order-table.php ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <h2>Printful product orders</h2>
3
+
4
+ <?php if ( ! empty( $orders ) && $orders['count'] > 0 ): ?>
5
+
6
+ <table class="wp-list-table widefat fixed striped printful-latest-orders">
7
+ <thead>
8
+ <tr>
9
+ <th class="col-order"><?php esc_html_e('Order', 'printful'); ?></th>
10
+ <th class="col-date"><?php esc_html_e('Date', 'printful'); ?></th>
11
+ <th class="col-from"><?php esc_html_e('From', 'printful'); ?></th>
12
+ <th class="col-status"><?php esc_html_e('Status', 'printful'); ?></th>
13
+ <th class="col-total"><?php esc_html_e('Total', 'printful'); ?></th>
14
+ <th class="col-actions"><?php esc_html_e('Actions', 'printful'); ?></th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+
19
+ <?php foreach ( $orders['results'] as $order ) : ?>
20
+
21
+ <tr>
22
+ <td>
23
+ <?php
24
+ if ( $order['external_id'] ) {
25
+ echo '<a href="' . esc_url( get_edit_post_link( $order['external_id'] ) ) . '">';
26
+ echo '#' . esc_html( $order['external_id'] );
27
+ echo '</a>';
28
+ } else {
29
+ echo '#' . esc_html( $order['id'] );
30
+ }
31
+ ?>
32
+ </td>
33
+ <td>
34
+ <?php echo esc_html( date('Y-m-d', $order['created']) ); ?>
35
+ </td>
36
+ <td>
37
+ <?php echo esc_html( $order['recipient']['name'] ); ?>
38
+ </td>
39
+ <td>
40
+ <?php echo esc_html( ucfirst($order['status']) ); ?>
41
+ </td>
42
+ <td>
43
+ $<?php echo esc_html( $order['costs']['total'] ); ?>
44
+ </td>
45
+ <td>
46
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard?order_id=<?php echo esc_attr($order['id']); ?>" target="_blank"><?php esc_html_e('Open in Printful', 'printful'); ?></a>
47
+ </td>
48
+ </tr>
49
+
50
+ <?php endforeach; ?>
51
+
52
+ </tbody>
53
+ <tfoot>
54
+ <tr>
55
+ <th class="col-order"><?php esc_html_e('Order', 'printful'); ?></th>
56
+ <th class="col-date"><?php esc_html_e('Date', 'printful'); ?></th>
57
+ <th class="col-from"><?php esc_html_e('From', 'printful'); ?></th>
58
+ <th class="col-status"><?php esc_html_e('Status', 'printful'); ?></th>
59
+ <th class="col-total"><?php esc_html_e('Total', 'printful'); ?></th>
60
+ <th class="col-actions"><?php esc_html_e('Actions', 'printful'); ?></th>
61
+ </tr>
62
+ </tfoot>
63
+ </table>
64
+
65
+ <?php else: ?>
66
+ <div class="printful-latest-orders">
67
+ <p><?php esc_html_e('Once your store gets some Printful product orders, they will be shown here!', 'printful'); ?></p>
68
+ </div>
69
+ <?php endif; ?>
trunk/includes/templates/personalize-button.php ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @var string $pfc_button_color
4
+ * @var string $site_url
5
+ * @var string $pfc_button_text
6
+ */
7
+ ?>
8
+ <a class="button"
9
+ style="background-color: <?php esc_attr_e($pfc_button_color); ?>"
10
+ onclick="Printful_Product_Customizer.onCustomizeClick( '<?php echo esc_url($site_url); ?>')">
11
+ <?php esc_html_e($pfc_button_text, 'printful'); ?>
12
+ </a>
trunk/includes/templates/quick-links.php ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <h2>Quick links</h2>
2
+
3
+ <div class="printful-quick-links">
4
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/default/orders" target="_blank" class="printful-quick-links-item">
5
+ <span class="dashicons dashicons-cart"></span>
6
+ <h4><?php esc_html_e('Orders', 'printful'); ?></h4>
7
+ </a>
8
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/library" target="_blank" class="printful-quick-links-item">
9
+ <span class="dashicons dashicons-images-alt"></span>
10
+ <h4><?php esc_html_e('File library', 'printful'); ?></h4>
11
+ </a>
12
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/store" target="_blank" class="printful-quick-links-item">
13
+ <span class="dashicons dashicons-store"></span>
14
+ <h4><?php esc_html_e('Stores', 'printful'); ?></h4>
15
+ </a>
16
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/reports" target="_blank" class="printful-quick-links-item">
17
+ <span class="dashicons dashicons-chart-area"></span>
18
+ <h4><?php esc_html_e('Reports', 'printful'); ?></h4>
19
+ </a>
20
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/settings/account-settings" target="_blank" class="printful-quick-links-item">
21
+ <span class="dashicons dashicons-admin-users"></span>
22
+ <h4><?php esc_html_e('My Account', 'printful'); ?></h4>
23
+ </a>
24
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/billing" target="_blank" class="printful-quick-links-item">
25
+ <span class="dashicons dashicons-portfolio"></span>
26
+ <h4><?php esc_html_e('Billing', 'printful'); ?></h4>
27
+ </a>
28
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>dashboard/notifications" target="_blank" class="printful-quick-links-item">
29
+ <span class="dashicons dashicons-megaphone"></span>
30
+ <h4><?php esc_html_e('Notifications', 'printful'); ?></h4>
31
+ </a>
32
+ </div>
trunk/includes/templates/setting-group.php ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @var string $title
4
+ * @var string $description
5
+ * @var string $carrier_version
6
+ * @var array $settings
7
+ */
8
+ ?>
9
+ <div class="printful-setting-group">
10
+
11
+ <h2><?php echo esc_html($title); ?></h2>
12
+ <p><?php _e( $description, 'printful' ); ?></p>
13
+
14
+ <?php if ( !empty( $settings ) ) : ?>
15
+
16
+ <table class="form-table">
17
+ <tbody>
18
+ <?php foreach($settings as $key => $setting) : ?>
19
+
20
+ <?php
21
+ if($setting['type'] == 'title') {
22
+ continue;
23
+ }
24
+ ?>
25
+
26
+ <tr valign="top">
27
+
28
+ <th scope="row" class="titledesc">
29
+ <label for="<?php echo esc_attr($key); ?>"><?php echo esc_html($setting['title']); ?></label>
30
+ </th>
31
+
32
+ <td class="forminp">
33
+ <fieldset>
34
+ <legend class="screen-reader-text"><span><?php echo esc_html($setting['title']); ?></span></legend>
35
+
36
+ <?php if ( $setting['type'] == 'text' ) : ?>
37
+
38
+ <input class="input-text regular-input" type="text" name="<?php echo esc_attr($key); ?>" id="<?php echo esc_attr($key); ?>" value="<?php echo esc_html($setting['value'] ?: $setting['default']); ?>" placeholder="">
39
+
40
+ <?php elseif ($setting['type'] == 'checkbox') : ?>
41
+
42
+ <label for="<?php echo esc_attr($key); ?>">
43
+ <input class="" type="checkbox" name="<?php echo esc_attr($key); ?>" id="<?php echo esc_attr($key); ?>" value="1" <?php if($setting['value'] == 'yes') { echo 'checked="checked"'; } ?>>
44
+ <?php _e( $setting['label'] , 'printful' ); ?>
45
+ </label>
46
+ <br>
47
+
48
+ <?php elseif ($setting['type'] == 'checkbox-group') : ?>
49
+
50
+ <?php foreach ( $setting['items'] as $checkbox_key => $item ) : ?>
51
+
52
+ <label class="carrier-type"><?php echo $item['subtitle']; ?></label>
53
+ <?php foreach ( $item['carriers'] as $carrier_name => $carrier ): ?>
54
+
55
+ <label for="<?php echo esc_attr( $key ) . '_' . esc_attr( $checkbox_key ).'_'.esc_attr( $carrier_name ); ?>" class="checkbox-group-item">
56
+ <input class="" type="checkbox" name="<?php echo esc_attr( $key ) . '[' . esc_attr( $checkbox_key ) . '][]'; ?>"
57
+ id="<?php echo esc_attr( $key ) . '_' .esc_attr( $checkbox_key ) . '_' . esc_attr( $carrier_name ); ?>"
58
+ value="<?php echo esc_attr( $key ) . '_' . esc_attr( $checkbox_key ) . '_' . esc_attr( $carrier_name ); ?>"
59
+ <?php if ( $carrier['value'] == 'yes' ) {
60
+ echo 'checked="checked"';
61
+ } ?>
62
+ <?php if ( $carrier['isDisabled'] ) {
63
+ echo 'disabled="disabled"';
64
+ } ?>
65
+ >
66
+ <?php echo wp_kses_post( $carrier['label'] ); ?>
67
+ </label>
68
+ <br>
69
+
70
+ <?php endforeach; ?>
71
+
72
+ <?php endforeach; ?>
73
+
74
+ <?php elseif ($setting['type'] == 'color-picker') : ?>
75
+ <label for="<?php esc_attr_e($key); ?>">
76
+ <input type="text" name="<?php esc_attr_e($key); ?>" class="<?php esc_attr_e($key); ?> pf-color-picker" value="<?php esc_attr_e($setting['value'] ?: $setting['default']); ?>" data-default-color="<?php esc_attr_e($setting['default']); ?>" />
77
+
78
+ <?php elseif ($setting['type'] == 'dropdown') : ?>
79
+ <label for="<?php esc_attr_e($key); ?>">
80
+ <select name="<?php esc_attr_e($key); ?>" id="<?php esc_attr_e($key); ?>">
81
+ <?php foreach ( $setting['items'] as $selectionKey => $selectionValue ) : ?>
82
+ <option <?php echo $setting['selected'] === $selectionKey ? 'selected' : ''; ?>
83
+ value="<?php esc_attr_e($selectionKey); ?>">
84
+ <?php esc_attr_e($selectionValue); ?>
85
+ </option>
86
+ <?php endforeach; ?>
87
+ </select>
88
+ <?php endif; ?>
89
+
90
+ </fieldset>
91
+ </td>
92
+ </tr>
93
+
94
+ <?php endforeach; ?>
95
+ </tbody>
96
+ </table>
97
+
98
+ <?php endif; ?>
99
+
100
+ </div>
101
+ <script>
102
+ jQuery(document).ready(function($){
103
+ $('.pf-color-picker').wpColorPicker();
104
+ });
105
+ </script>
trunk/includes/templates/setting-submit.php ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @var bool $disabled
4
+ */
5
+ ?>
6
+ <p class="printful-submit">
7
+ <input name="save" class="button-primary woocommerce-save-button <?php if($disabled) { echo 'disabled'; } ?>" type="submit" value="<?php esc_attr_e('Save changes', 'printful'); ?>" <?php if($disabled) { echo 'disabled'; } ?>/>
8
+ <input type="hidden" id="_wpnonce" name="_wpnonce" value="<?php echo esc_attr($nonce); ?>">
9
+ <?php wp_referer_field(true); ?>
10
+ <span class="loader-wrap">
11
+ <img src="<?php echo esc_url( admin_url( 'images/spinner-2x.gif' ) ) ?>" class="loader" width="20px" height="20px" alt="loader"/>
12
+ <span class="pass">
13
+ <span class="dashicons dashicons-yes"></span>
14
+ <?php esc_html_e('Saved successfully', 'printful'); ?>
15
+ </span>
16
+ <span class="fail">
17
+ </span>
18
+ </span>
19
+ </p>
20
+ <script type="text/javascript">
21
+ jQuery(document).ready(function () {
22
+ Printful_Settings.init_submit();
23
+ });
24
+ </script>
trunk/includes/templates/shipping-notification.php ADDED
@@ -0,0 +1,4 @@
 
 
 
 
1
+ <div class="printful-setting-group">
2
+ <h2><?php esc_html_e('Printful Shipping', 'printful'); ?></h2>
3
+ <p><?php esc_html_e('To enable/disable Printful shipping for your store go to', 'printful'); ?> <a href="<?php echo esc_url( admin_url( 'admin.php?page=wc-settings&tab=shipping&section=printful_shipping' ) ); ?>"><?php esc_html_e('WooCommerce Shipping settings', 'printful'); ?></a>.</p>
4
+ </div>
trunk/includes/templates/size-guide-button.php ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * @var string $size_guide_button_color
4
+ * @var string $size_guide_button_text
5
+ */
6
+ ?>
7
+ <a href="javascript:" style="color: <?php esc_attr_e($size_guide_button_color); ?>"
8
+ onclick="Printful_Product_Size_Guide.onSizeGuideClick()">
9
+ <?php esc_html_e($size_guide_button_text, 'printful'); ?>
10
+ </a>
trunk/includes/templates/stats.php ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="printful-stats">
2
+ <div class="printful-stats-item">
3
+ <h4><?php echo esc_html(get_woocommerce_currency_symbol($stats['currency'])) . ' ' . esc_html($stats['orders_today']['total']); ?></h4>
4
+ <b>
5
+ <?php
6
+ echo esc_html($stats['orders_today']['orders']);
7
+ echo ' ' . _n('ORDER', 'ORDERS', $stats['orders_today']['orders'], 'printful' );
8
+ ?>
9
+ </b>
10
+ <?php esc_html_e('today', 'printful'); ?>
11
+ </div>
12
+ <div class="printful-stats-item">
13
+ <h4>
14
+ <?php echo esc_html(get_woocommerce_currency_symbol($stats['currency'])) . ' ' . esc_html($stats['orders_last_7_days']['total']); ?>
15
+ <?php echo '<span class="dashicons dashicons-arrow-' . esc_attr($stats['orders_last_7_days']['trend']) .'-alt"></span>'; ?>
16
+ </h4>
17
+ <b>
18
+ <?php
19
+ echo esc_html($stats['orders_last_7_days']['orders']);
20
+ echo ' ' . _n( 'ORDER', 'ORDERS', $stats['orders_last_7_days']['orders'], 'printful' );
21
+ ?>
22
+ </b>
23
+ <?php esc_html_e('last 7 days', 'printful'); ?>
24
+ </div>
25
+ <div class="printful-stats-item">
26
+ <h4>
27
+ <?php echo esc_html(get_woocommerce_currency_symbol($stats['currency'])) . ' ' . esc_html($stats['orders_last_28_days']['total']); ?>
28
+ <?php echo '<span class="dashicons dashicons-arrow-' . esc_attr($stats['orders_last_28_days']['trend']) .'-alt"></span>'; ?>
29
+ </h4>
30
+ <b>
31
+ <?php
32
+ echo esc_html($stats['orders_last_28_days']['orders']);
33
+ echo ' ' . _n( 'ORDER', 'ORDERS', $stats['orders_last_28_days']['orders'], 'printful' );
34
+ ?>
35
+ </b> <?php esc_html_e('last 28 days', 'printful'); ?>
36
+ </div>
37
+ <div class="printful-stats-item">
38
+ <h4>
39
+ <?php echo esc_html(get_woocommerce_currency_symbol($stats['currency'])) . ' ' . esc_attr($stats['profit_last_28_days']); ?>
40
+ <?php echo '<span class="dashicons dashicons-arrow-' . esc_attr($stats['profit_trend_last_28_days']) .'-alt"></span>'; ?>
41
+ </h4>
42
+ <b><?php esc_html_e('PROFIT', 'printful'); ?></b> <?php esc_html_e('last 28 days', 'printful'); ?>
43
+ </div>
44
+ </div>
trunk/includes/templates/status-report.php ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="support-report-wrap">
2
+ <p>
3
+ <?php esc_html_e('Copy the box content below and add it to your support message', 'printful'); ?>
4
+ <br/>
5
+ <?php esc_html_e('Note: this status report may not include an error log. Contact your hosting provider if you need help with acquiring error logs.'); ?>
6
+ </p>
7
+ <textarea class="support-report"><?php echo esc_html($status_report); ?></textarea>
8
+ <button class="button button-primary button-large support-report-btn"><?php esc_html_e('Copy', 'printful'); ?></button>
9
+ <script type="text/javascript">
10
+ var copyTextareaBtn = document.querySelector('.support-report-btn');
11
+
12
+ copyTextareaBtn.addEventListener('click', function() {
13
+ var copyTextarea = document.querySelector('.support-report');
14
+ copyTextarea.select();
15
+
16
+ try {
17
+ document.execCommand('copy');
18
+ } catch (err) {
19
+ //do nothing
20
+ }
21
+ });
22
+ </script>
23
+ </div>
trunk/includes/templates/status-table.php ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php if ( $checklist['overall_status'] ) {
2
+ ?>
3
+ <div class="notice notice-success">
4
+ <p><?php esc_html_e('Looks like the everything is set up correctly and Printful integration should work as intended.', 'printful'); ?></p>
5
+ </div>
6
+ <?php
7
+ } else {
8
+ ?>
9
+ <div class="notice notice-error">
10
+ <p><?php esc_html_e('There are errors with your store setup that may cause the Printful integration to not work as intended!', 'printful'); ?></p>
11
+ </div>
12
+ <?php
13
+ }
14
+ ?>
15
+
16
+ <table class="wp-list-table widefat fixed striped printful-status">
17
+ <thead>
18
+ <tr>
19
+ <td class="col-name"><?php esc_html_e('Name', 'printful'); ?></td>
20
+ <td class="col-desc"><?php esc_html_e('Description', 'printful'); ?></td>
21
+ <td class="col-status"><?php esc_html_e('Status', 'printful'); ?></td>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <?php
26
+ foreach ( $checklist['items'] as $item ) : ?>
27
+ <tr>
28
+ <td><?php echo esc_html( $item['name'] ); ?></td>
29
+ <td><?php echo esc_html( $item['description'] ); ?></td>
30
+ <td>
31
+ <?php
32
+ $status = 'OK';
33
+ if ( $item['status'] == 1 ) {
34
+ echo '<span class="pass">' . esc_html__('OK', 'printful') .'</span>';
35
+ } else if ( $item['status'] == 0 ) {
36
+ echo '<span class="warning">' . esc_html__('WARNING', 'printful') .'&#42;</span>';
37
+ } else if ( $item['status'] == 2 ) {
38
+ echo '<span class="fail">' . esc_html__('NOT CONNECTED', 'printful') .'</span>';
39
+ } else {
40
+ echo '<span class="fail">' . esc_html__('FAIL', 'printful') .'</span>';
41
+ }
42
+ ?>
43
+ </td>
44
+ </tr>
45
+ <?php endforeach; ?>
46
+ </tbody>
47
+ <tfoot>
48
+ <tr>
49
+ <td class="col-name"><?php esc_html_e('Name', 'printful'); ?></td>
50
+ <td class="col-desc"><?php esc_html_e('Description', 'printful'); ?></td>
51
+ <td class="col-status"><?php esc_html_e('Status', 'printful'); ?></td>
52
+ </tr>
53
+ </tfoot>
54
+ </table>
55
+
56
+ <p class="asterisk">&#42; <?php esc_html_e('Warnings are issued when the test was unable to come to a definite conclusion or if the result was passable, but not ideal.', 'printful'); ?></p>
trunk/includes/templates/support-info.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="support-info-wrap">
2
+
3
+ <div class="support-info-block">
4
+ <h3><?php esc_html_e('Need help? Get in touch!', 'printful') ?></h3>
5
+ <p><?php esc_html_e('Email, call, text, or visit us. We\'ve got support teams in the USA and Europe to make sure we\'re there when you need us.', 'printful') ?><br/><?php esc_html_e('We\'d love to hear from you.', 'printful') ?></p>
6
+ <div class="btn-wrap">
7
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>contacts" class="button button-primary button-large" target="_blank"><?php esc_html_e('Contact support', 'printful') ?></a>
8
+ </div>
9
+ </div>
10
+
11
+ <div class="support-info-block">
12
+ <h3><?php esc_html_e('Read our FAQs', 'printful') ?></h3>
13
+ <p><?php esc_html_e('Getting started made easy – read the FAQs to jumpstart your business.', 'printful') ?><br/><?php esc_html_e('Whether you\'re a video tutorial fan or prefer the written answers – we\'ve got it covered!', 'printful') ?></p>
14
+ <div class="btn-wrap">
15
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>faq" class="button button-primary button-large" target="_blank"><?php esc_html_e('See Printful FAQ', 'printful') ?></a>
16
+ </div>
17
+ </div>
18
+
19
+ <div class="support-info-block">
20
+ <h3><?php esc_html_e('Integration help', 'printful') ?></h3>
21
+ <p><?php esc_html_e('Are you experiencing technical issues? Solve them on your own by reading these helpful guides and video tutorials.', 'printful') ?></p>
22
+ <div class="btn-wrap">
23
+ <a href="<?php echo esc_url(Printful_Base::get_printful_host()); ?>faq/integrations/woocommerce" class="button button-primary button-large" target="_blank"><?php esc_html_e('Integration help', 'printful') ?></a>
24
+ </div>
25
+ </div>
26
+
27
+ </div>
trunk/license.txt ADDED
@@ -0,0 +1,697 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Printful Integration for WooCommerce
2
+
3
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4
+
5
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6
+
7
+ You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/
8
+
9
+ This program incorporates work covered by the following copyright and
10
+ permission notices:
11
+
12
+ intercom.js is Copyright (c) 2012 DIY Co
13
+ https://github.com/diy/intercom.js
14
+ intercom.js is released under Apache License (v2)
15
+
16
+ and
17
+
18
+ Printful Integration for WooCommerce
19
+ Copyright (c) 2022 Printful® Inc
20
+ Printful Integration for WooCommerce is released under GPL
21
+
22
+ =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
23
+
24
+ GNU GENERAL PUBLIC LICENSE
25
+ Version 3, 29 June 2007
26
+
27
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
28
+ Everyone is permitted to copy and distribute verbatim copies
29
+ of this license document, but changing it is not allowed.
30
+
31
+ Preamble
32
+
33
+ The GNU General Public License is a free, copyleft license for
34
+ software and other kinds of works.
35
+
36
+ The licenses for most software and other practical works are designed
37
+ to take away your freedom to share and change the works. By contrast,
38
+ the GNU General Public License is intended to guarantee your freedom to
39
+ share and change all versions of a program--to make sure it remains free
40
+ software for all its users. We, the Free Software Foundation, use the
41
+ GNU General Public License for most of our software; it applies also to
42
+ any other work released this way by its authors. You can apply it to
43
+ your programs, too.
44
+
45
+ When we speak of free software, we are referring to freedom, not
46
+ price. Our General Public Licenses are designed to make sure that you
47
+ have the freedom to distribute copies of free software (and charge for
48
+ them if you wish), that you receive source code or can get it if you
49
+ want it, that you can change the software or use pieces of it in new
50
+ free programs, and that you know you can do these things.
51
+
52
+ To protect your rights, we need to prevent others from denying you
53
+ these rights or asking you to surrender the rights. Therefore, you have
54
+ certain responsibilities if you distribute copies of the software, or if
55
+ you modify it: responsibilities to respect the freedom of others.
56
+
57
+ For example, if you distribute copies of such a program, whether
58
+ gratis or for a fee, you must pass on to the recipients the same
59
+ freedoms that you received. You must make sure that they, too, receive
60
+ or can get the source code. And you must show them these terms so they
61
+ know their rights.
62
+
63
+ Developers that use the GNU GPL protect your rights with two steps:
64
+ (1) assert copyright on the software, and (2) offer you this License
65
+ giving you legal permission to copy, distribute and/or modify it.
66
+
67
+ For the developers' and authors' protection, the GPL clearly explains
68
+ that there is no warranty for this free software. For both users' and
69
+ authors' sake, the GPL requires that modified versions be marked as
70
+ changed, so that their problems will not be attributed erroneously to
71
+ authors of previous versions.
72
+
73
+ Some devices are designed to deny users access to install or run
74
+ modified versions of the software inside them, although the manufacturer
75
+ can do so. This is fundamentally incompatible with the aim of
76
+ protecting users' freedom to change the software. The systematic
77
+ pattern of such abuse occurs in the area of products for individuals to
78
+ use, which is precisely where it is most unacceptable. Therefore, we
79
+ have designed this version of the GPL to prohibit the practice for those
80
+ products. If such problems arise substantially in other domains, we
81
+ stand ready to extend this provision to those domains in future versions
82
+ of the GPL, as needed to protect the freedom of users.
83
+
84
+ Finally, every program is threatened constantly by software patents.
85
+ States should not allow patents to restrict development and use of
86
+ software on general-purpose computers, but in those that do, we wish to
87
+ avoid the special danger that patents applied to a free program could
88
+ make it effectively proprietary. To prevent this, the GPL assures that
89
+ patents cannot be used to render the program non-free.
90
+
91
+ The precise terms and conditions for copying, distribution and
92
+ modification follow.
93
+
94
+ TERMS AND CONDITIONS
95
+
96
+ 0. Definitions.
97
+
98
+ "This License" refers to version 3 of the GNU General Public License.
99
+
100
+ "Copyright" also means copyright-like laws that apply to other kinds of
101
+ works, such as semiconductor masks.
102
+
103
+ "The Program" refers to any copyrightable work licensed under this
104
+ License. Each licensee is addressed as "you". "Licensees" and
105
+ "recipients" may be individuals or organizations.
106
+
107
+ To "modify" a work means to copy from or adapt all or part of the work
108
+ in a fashion requiring copyright permission, other than the making of an
109
+ exact copy. The resulting work is called a "modified version" of the
110
+ earlier work or a work "based on" the earlier work.
111
+
112
+ A "covered work" means either the unmodified Program or a work based
113
+ on the Program.
114
+
115
+ To "propagate" a work means to do anything with it that, without
116
+ permission, would make you directly or secondarily liable for
117
+ infringement under applicable copyright law, except executing it on a
118
+ computer or modifying a private copy. Propagation includes copying,
119
+ distribution (with or without modification), making available to the
120
+ public, and in some countries other activities as well.
121
+
122
+ To "convey" a work means any kind of propagation that enables other
123
+ parties to make or receive copies. Mere interaction with a user through
124
+ a computer network, with no transfer of a copy, is not conveying.
125
+
126
+ An interactive user interface displays "Appropriate Legal Notices"
127
+ to the extent that it includes a convenient and prominently visible
128
+ feature that (1) displays an appropriate copyright notice, and (2)
129
+ tells the user that there is no warranty for the work (except to the
130
+ extent that warranties are provided), that licensees may convey the
131
+ work under this License, and how to view a copy of this License. If
132
+ the interface presents a list of user commands or options, such as a
133
+ menu, a prominent item in the list meets this criterion.
134
+
135
+ 1. Source Code.
136
+
137
+ The "source code" for a work means the preferred form of the work
138
+ for making modifications to it. "Object code" means any non-source
139
+ form of a work.
140
+
141
+ A "Standard Interface" means an interface that either is an official
142
+ standard defined by a recognized standards body, or, in the case of
143
+ interfaces specified for a particular programming language, one that
144
+ is widely used among developers working in that language.
145
+
146
+ The "System Libraries" of an executable work include anything, other
147
+ than the work as a whole, that (a) is included in the normal form of
148
+ packaging a Major Component, but which is not part of that Major
149
+ Component, and (b) serves only to enable use of the work with that
150
+ Major Component, or to implement a Standard Interface for which an
151
+ implementation is available to the public in source code form. A
152
+ "Major Component", in this context, means a major essential component
153
+ (kernel, window system, and so on) of the specific operating system
154
+ (if any) on which the executable work runs, or a compiler used to
155
+ produce the work, or an object code interpreter used to run it.
156
+
157
+ The "Corresponding Source" for a work in object code form means all
158
+ the source code needed to generate, install, and (for an executable
159
+ work) run the object code and to modify the work, including scripts to
160
+ control those activities. However, it does not include the work's
161
+ System Libraries, or general-purpose tools or generally available free
162
+ programs which are used unmodified in performing those activities but
163
+ which are not part of the work. For example, Corresponding Source
164
+ includes interface definition files associated with source files for
165
+ the work, and the source code for shared libraries and dynamically
166
+ linked subprograms that the work is specifically designed to require,
167
+ such as by intimate data communication or control flow between those
168
+ subprograms and other parts of the work.
169
+
170
+ The Corresponding Source need not include anything that users
171
+ can regenerate automatically from other parts of the Corresponding
172
+ Source.
173
+
174
+ The Corresponding Source for a work in source code form is that
175
+ same work.
176
+
177
+ 2. Basic Permissions.
178
+
179
+ All rights granted under this License are granted for the term of
180
+ copyright on the Program, and are irrevocable provided the stated
181
+ conditions are met. This License explicitly affirms your unlimited
182
+ permission to run the unmodified Program. The output from running a
183
+ covered work is covered by this License only if the output, given its
184
+ content, constitutes a covered work. This License acknowledges your
185
+ rights of fair use or other equivalent, as provided by copyright law.
186
+
187
+ You may make, run and propagate covered works that you do not
188
+ convey, without conditions so long as your license otherwise remains
189
+ in force. You may convey covered works to others for the sole purpose
190
+ of having them make modifications exclusively for you, or provide you
191
+ with facilities for running those works, provided that you comply with
192
+ the terms of this License in conveying all material for which you do
193
+ not control copyright. Those thus making or running the covered works
194
+ for you must do so exclusively on your behalf, under your direction
195
+ and control, on terms that prohibit them from making any copies of
196
+ your copyrighted material outside their relationship with you.
197
+
198
+ Conveying under any other circumstances is permitted solely under
199
+ the conditions stated below. Sublicensing is not allowed; section 10
200
+ makes it unnecessary.
201
+
202
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
203
+
204
+ No covered work shall be deemed part of an effective technological
205
+ measure under any applicable law fulfilling obligations under article
206
+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
207
+ similar laws prohibiting or restricting circumvention of such
208
+ measures.
209
+
210
+ When you convey a covered work, you waive any legal power to forbid
211
+ circumvention of technological measures to the extent such circumvention
212
+ is effected by exercising rights under this License with respect to
213
+ the covered work, and you disclaim any intention to limit operation or
214
+ modification of the work as a means of enforcing, against the work's
215
+ users, your or third parties' legal rights to forbid circumvention of
216
+ technological measures.
217
+
218
+ 4. Conveying Verbatim Copies.
219
+
220
+ You may convey verbatim copies of the Program's source code as you
221
+ receive it, in any medium, provided that you conspicuously and
222
+ appropriately publish on each copy an appropriate copyright notice;
223
+ keep intact all notices stating that this License and any
224
+ non-permissive terms added in accord with section 7 apply to the code;
225
+ keep intact all notices of the absence of any warranty; and give all
226
+ recipients a copy of this License along with the Program.
227
+
228
+ You may charge any price or no price for each copy that you convey,
229
+ and you may offer support or warranty protection for a fee.
230
+
231
+ 5. Conveying Modified Source Versions.
232
+
233
+ You may convey a work based on the Program, or the modifications to
234
+ produce it from the Program, in the form of source code under the
235
+ terms of section 4, provided that you also meet all of these conditions:
236
+
237
+ a) The work must carry prominent notices stating that you modified
238
+ it, and giving a relevant date.
239
+
240
+ b) The work must carry prominent notices stating that it is
241
+ released under this License and any conditions added under section
242
+ 7. This requirement modifies the requirement in section 4 to
243
+ "keep intact all notices".
244
+
245
+ c) You must license the entire work, as a whole, under this
246
+ License to anyone who comes into possession of a copy. This
247
+ License will therefore apply, along with any applicable section 7
248
+ additional terms, to the whole of the work, and all its parts,
249
+ regardless of how they are packaged. This License gives no
250
+ permission to license the work in any other way, but it does not
251
+ invalidate such permission if you have separately received it.
252
+
253
+ d) If the work has interactive user interfaces, each must display
254
+ Appropriate Legal Notices; however, if the Program has interactive
255
+ interfaces that do not display Appropriate Legal Notices, your
256
+ work need not make them do so.
257
+
258
+ A compilation of a covered work with other separate and independent
259
+ works, which are not by their nature extensions of the covered work,
260
+ and which are not combined with it such as to form a larger program,
261
+ in or on a volume of a storage or distribution medium, is called an
262
+ "aggregate" if the compilation and its resulting copyright are not
263
+ used to limit the access or legal rights of the compilation's users
264
+ beyond what the individual works permit. Inclusion of a covered work
265
+ in an aggregate does not cause this License to apply to the other
266
+ parts of the aggregate.
267
+
268
+ 6. Conveying Non-Source Forms.
269
+
270
+ You may convey a covered work in object code form under the terms
271
+ of sections 4 and 5, provided that you also convey the
272
+ machine-readable Corresponding Source under the terms of this License,
273
+ in one of these ways:
274
+
275
+ a) Convey the object code in, or embodied in, a physical product
276
+ (including a physical distribution medium), accompanied by the
277
+ Corresponding Source fixed on a durable physical medium
278
+ customarily used for software interchange.
279
+
280
+ b) Convey the object code in, or embodied in, a physical product
281
+ (including a physical distribution medium), accompanied by a
282
+ written offer, valid for at least three years and valid for as
283
+ long as you offer spare parts or customer support for that product
284
+ model, to give anyone who possesses the object code either (1) a
285
+ copy of the Corresponding Source for all the software in the
286
+ product that is covered by this License, on a durable physical
287
+ medium customarily used for software interchange, for a price no
288
+ more than your reasonable cost of physically performing this
289
+ conveying of source, or (2) access to copy the
290
+ Corresponding Source from a network server at no charge.
291
+
292
+ c) Convey individual copies of the object code with a copy of the
293
+ written offer to provide the Corresponding Source. This
294
+ alternative is allowed only occasionally and noncommercially, and
295
+ only if you received the object code with such an offer, in accord
296
+ with subsection 6b.
297
+
298
+ d) Convey the object code by offering access from a designated
299
+ place (gratis or for a charge), and offer equivalent access to the
300
+ Corresponding Source in the same way through the same place at no
301
+ further charge. You need not require recipients to copy the
302
+ Corresponding Source along with the object code. If the place to
303
+ copy the object code is a network server, the Corresponding Source
304
+ may be on a different server (operated by you or a third party)
305
+ that supports equivalent copying facilities, provided you maintain
306
+ clear directions next to the object code saying where to find the
307
+ Corresponding Source. Regardless of what server hosts the
308
+ Corresponding Source, you remain obligated to ensure that it is
309
+ available for as long as needed to satisfy these requirements.
310
+
311
+ e) Convey the object code using peer-to-peer transmission, provided
312
+ you inform other peers where the object code and Corresponding
313
+ Source of the work are being offered to the general public at no
314
+ charge under subsection 6d.
315
+
316
+ A separable portion of the object code, whose source code is excluded
317
+ from the Corresponding Source as a System Library, need not be
318
+ included in conveying the object code work.
319
+
320
+ A "User Product" is either (1) a "consumer product", which means any
321
+ tangible personal property which is normally used for personal, family,
322
+ or household purposes, or (2) anything designed or sold for incorporation
323
+ into a dwelling. In determining whether a product is a consumer product,
324
+ doubtful cases shall be resolved in favor of coverage. For a particular
325
+ product received by a particular user, "normally used" refers to a
326
+ typical or common use of that class of product, regardless of the status
327
+ of the particular user or of the way in which the particular user
328
+ actually uses, or expects or is expected to use, the product. A product
329
+ is a consumer product regardless of whether the product has substantial
330
+ commercial, industrial or non-consumer uses, unless such uses represent
331
+ the only significant mode of use of the product.
332
+
333
+ "Installation Information" for a User Product means any methods,
334
+ procedures, authorization keys, or other information required to install
335
+ and execute modified versions of a covered work in that User Product from
336
+ a modified version of its Corresponding Source. The information must
337
+ suffice to ensure that the continued functioning of the modified object
338
+ code is in no case prevented or interfered with solely because
339
+ modification has been made.
340
+
341
+ If you convey an object code work under this section in, or with, or
342
+ specifically for use in, a User Product, and the conveying occurs as
343
+ part of a transaction in which the right of possession and use of the
344
+ User Product is transferred to the recipient in perpetuity or for a
345
+ fixed term (regardless of how the transaction is characterized), the
346
+ Corresponding Source conveyed under this section must be accompanied
347
+ by the Installation Information. But this requirement does not apply
348
+ if neither you nor any third party retains the ability to install
349
+ modified object code on the User Product (for example, the work has
350
+ been installed in ROM).
351
+
352
+ The requirement to provide Installation Information does not include a
353
+ requirement to continue to provide support service, warranty, or updates
354
+ for a work that has been modified or installed by the recipient, or for
355
+ the User Product in which it has been modified or installed. Access to a
356
+ network may be denied when the modification itself materially and
357
+ adversely affects the operation of the network or violates the rules and
358
+ protocols for communication across the network.
359
+
360
+ Corresponding Source conveyed, and Installation Information provided,
361
+ in accord with this section must be in a format that is publicly
362
+ documented (and with an implementation available to the public in
363
+ source code form), and must require no special password or key for
364
+ unpacking, reading or copying.
365
+
366
+ 7. Additional Terms.
367
+
368
+ "Additional permissions" are terms that supplement the terms of this
369
+ License by making exceptions from one or more of its conditions.
370
+ Additional permissions that are applicable to the entire Program shall
371
+ be treated as though they were included in this License, to the extent
372
+ that they are valid under applicable law. If additional permissions
373
+ apply only to part of the Program, that part may be used separately
374
+ under those permissions, but the entire Program remains governed by
375
+ this License without regard to the additional permissions.
376
+
377
+ When you convey a copy of a covered work, you may at your option
378
+ remove any additional permissions from that copy, or from any part of
379
+ it. (Additional permissions may be written to require their own
380
+ removal in certain cases when you modify the work.) You may place
381
+ additional permissions on material, added by you to a covered work,
382
+ for which you have or can give appropriate copyright permission.
383
+
384
+ Notwithstanding any other provision of this License, for material you
385
+ add to a covered work, you may (if authorized by the copyright holders of
386
+ that material) supplement the terms of this License with terms:
387
+
388
+ a) Disclaiming warranty or limiting liability differently from the
389
+ terms of sections 15 and 16 of this License; or
390
+
391
+ b) Requiring preservation of specified reasonable legal notices or
392
+ author attributions in that material or in the Appropriate Legal
393
+ Notices displayed by works containing it; or
394
+
395
+ c) Prohibiting misrepresentation of the origin of that material, or
396
+ requiring that modified versions of such material be marked in
397
+ reasonable ways as different from the original version; or
398
+
399
+ d) Limiting the use for publicity purposes of names of licensors or
400
+ authors of the material; or
401
+
402
+ e) Declining to grant rights under trademark law for use of some
403
+ trade names, trademarks, or service marks; or
404
+
405
+ f) Requiring indemnification of licensors and authors of that
406
+ material by anyone who conveys the material (or modified versions of
407
+ it) with contractual assumptions of liability to the recipient, for
408
+ any liability that these contractual assumptions directly impose on
409
+ those licensors and authors.
410
+
411
+ All other non-permissive additional terms are considered "further
412
+ restrictions" within the meaning of section 10. If the Program as you
413
+ received it, or any part of it, contains a notice stating that it is
414
+ governed by this License along with a term that is a further
415
+ restriction, you may remove that term. If a license document contains
416
+ a further restriction but permits relicensing or conveying under this
417
+ License, you may add to a covered work material governed by the terms
418
+ of that license document, provided that the further restriction does
419
+ not survive such relicensing or conveying.
420
+
421
+ If you add terms to a covered work in accord with this section, you
422
+ must place, in the relevant source files, a statement of the
423
+ additional terms that apply to those files, or a notice indicating
424
+ where to find the applicable terms.
425
+
426
+ Additional terms, permissive or non-permissive, may be stated in the
427
+ form of a separately written license, or stated as exceptions;
428
+ the above requirements apply either way.
429
+
430
+ 8. Termination.
431
+
432
+ You may not propagate or modify a covered work except as expressly
433
+ provided under this License. Any attempt otherwise to propagate or
434
+ modify it is void, and will automatically terminate your rights under
435
+ this License (including any patent licenses granted under the third
436
+ paragraph of section 11).
437
+
438
+ However, if you cease all violation of this License, then your
439
+ license from a particular copyright holder is reinstated (a)
440
+ provisionally, unless and until the copyright holder explicitly and
441
+ finally terminates your license, and (b) permanently, if the copyright
442
+ holder fails to notify you of the violation by some reasonable means
443
+ prior to 60 days after the cessation.
444
+
445
+ Moreover, your license from a particular copyright holder is
446
+ reinstated permanently if the copyright holder notifies you of the
447
+ violation by some reasonable means, this is the first time you have
448
+ received notice of violation of this License (for any work) from that
449
+ copyright holder, and you cure the violation prior to 30 days after
450
+ your receipt of the notice.
451
+
452
+ Termination of your rights under this section does not terminate the
453
+ licenses of parties who have received copies or rights from you under
454
+ this License. If your rights have been terminated and not permanently
455
+ reinstated, you do not qualify to receive new licenses for the same
456
+ material under section 10.
457
+
458
+ 9. Acceptance Not Required for Having Copies.
459
+
460
+ You are not required to accept this License in order to receive or
461
+ run a copy of the Program. Ancillary propagation of a covered work
462
+ occurring solely as a consequence of using peer-to-peer transmission
463
+ to receive a copy likewise does not require acceptance. However,
464
+ nothing other than this License grants you permission to propagate or
465
+ modify any covered work. These actions infringe copyright if you do
466
+ not accept this License. Therefore, by modifying or propagating a
467
+ covered work, you indicate your acceptance of this License to do so.
468
+
469
+ 10. Automatic Licensing of Downstream Recipients.
470
+
471
+ Each time you convey a covered work, the recipient automatically
472
+ receives a license from the original licensors, to run, modify and
473
+ propagate that work, subject to this License. You are not responsible
474
+ for enforcing compliance by third parties with this License.
475
+
476
+ An "entity transaction" is a transaction transferring control of an
477
+ organization, or substantially all assets of one, or subdividing an
478
+ organization, or merging organizations. If propagation of a covered
479
+ work results from an entity transaction, each party to that
480
+ transaction who receives a copy of the work also receives whatever
481
+ licenses to the work the party's predecessor in interest had or could
482
+ give under the previous paragraph, plus a right to possession of the
483
+ Corresponding Source of the work from the predecessor in interest, if
484
+ the predecessor has it or can get it with reasonable efforts.
485
+
486
+ You may not impose any further restrictions on the exercise of the
487
+ rights granted or affirmed under this License. For example, you may
488
+ not impose a license fee, royalty, or other charge for exercise of
489
+ rights granted under this License, and you may not initiate litigation
490
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
491
+ any patent claim is infringed by making, using, selling, offering for
492
+ sale, or importing the Program or any portion of it.
493
+
494
+ 11. Patents.
495
+
496
+ A "contributor" is a copyright holder who authorizes use under this
497
+ License of the Program or a work on which the Program is based. The
498
+ work thus licensed is called the contributor's "contributor version".
499
+
500
+ A contributor's "essential patent claims" are all patent claims
501
+ owned or controlled by the contributor, whether already acquired or
502
+ hereafter acquired, that would be infringed by some manner, permitted
503
+ by this License, of making, using, or selling its contributor version,
504
+ but do not include claims that would be infringed only as a
505
+ consequence of further modification of the contributor version. For
506
+ purposes of this definition, "control" includes the right to grant
507
+ patent sublicenses in a manner consistent with the requirements of
508
+ this License.
509
+
510
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
511
+ patent license under the contributor's essential patent claims, to
512
+ make, use, sell, offer for sale, import and otherwise run, modify and
513
+ propagate the contents of its contributor version.
514
+
515
+ In the following three paragraphs, a "patent license" is any express
516
+ agreement or commitment, however denominated, not to enforce a patent
517
+ (such as an express permission to practice a patent or covenant not to
518
+ sue for patent infringement). To "grant" such a patent license to a
519
+ party means to make such an agreement or commitment not to enforce a
520
+ patent against the party.
521
+
522
+ If you convey a covered work, knowingly relying on a patent license,
523
+ and the Corresponding Source of the work is not available for anyone
524
+ to copy, free of charge and under the terms of this License, through a
525
+ publicly available network server or other readily accessible means,
526
+ then you must either (1) cause the Corresponding Source to be so
527
+ available, or (2) arrange to deprive yourself of the benefit of the
528
+ patent license for this particular work, or (3) arrange, in a manner
529
+ consistent with the requirements of this License, to extend the patent
530
+ license to downstream recipients. "Knowingly relying" means you have
531
+ actual knowledge that, but for the patent license, your conveying the
532
+ covered work in a country, or your recipient's use of the covered work
533
+ in a country, would infringe one or more identifiable patents in that
534
+ country that you have reason to believe are valid.
535
+
536
+ If, pursuant to or in connection with a single transaction or
537
+ arrangement, you convey, or propagate by procuring conveyance of, a
538
+ covered work, and grant a patent license to some of the parties
539
+ receiving the covered work authorizing them to use, propagate, modify
540
+ or convey a specific copy of the covered work, then the patent license
541
+ you grant is automatically extended to all recipients of the covered
542
+ work and works based on it.
543
+
544
+ A patent license is "discriminatory" if it does not include within
545
+ the scope of its coverage, prohibits the exercise of, or is
546
+ conditioned on the non-exercise of one or more of the rights that are
547
+ specifically granted under this License. You may not convey a covered
548
+ work if you are a party to an arrangement with a third party that is
549
+ in the business of distributing software, under which you make payment
550
+ to the third party based on the extent of your activity of conveying
551
+ the work, and under which the third party grants, to any of the
552
+ parties who would receive the covered work from you, a discriminatory
553
+ patent license (a) in connection with copies of the covered work
554
+ conveyed by you (or copies made from those copies), or (b) primarily
555
+ for and in connection with specific products or compilations that
556
+ contain the covered work, unless you entered into that arrangement,
557
+ or that patent license was granted, prior to 28 March 2007.
558
+
559
+ Nothing in this License shall be construed as excluding or limiting
560
+ any implied license or other defenses to infringement that may
561
+ otherwise be available to you under applicable patent law.
562
+
563
+ 12. No Surrender of Others' Freedom.
564
+
565
+ If conditions are imposed on you (whether by court order, agreement or
566
+ otherwise) that contradict the conditions of this License, they do not
567
+ excuse you from the conditions of this License. If you cannot convey a
568
+ covered work so as to satisfy simultaneously your obligations under this
569
+ License and any other pertinent obligations, then as a consequence you may
570
+ not convey it at all. For example, if you agree to terms that obligate you
571
+ to collect a royalty for further conveying from those to whom you convey
572
+ the Program, the only way you could satisfy both those terms and this
573
+ License would be to refrain entirely from conveying the Program.
574
+
575
+ 13. Use with the GNU Affero General Public License.
576
+
577
+ Notwithstanding any other provision of this License, you have
578
+ permission to link or combine any covered work with a work licensed
579
+ under version 3 of the GNU Affero General Public License into a single
580
+ combined work, and to convey the resulting work. The terms of this
581
+ License will continue to apply to the part which is the covered work,
582
+ but the special requirements of the GNU Affero General Public License,
583
+ section 13, concerning interaction through a network will apply to the
584
+ combination as such.
585
+
586
+ 14. Revised Versions of this License.
587
+
588
+ The Free Software Foundation may publish revised and/or new versions of
589
+ the GNU General Public License from time to time. Such new versions will
590
+ be similar in spirit to the present version, but may differ in detail to
591
+ address new problems or concerns.
592
+
593
+ Each version is given a distinguishing version number. If the
594
+ Program specifies that a certain numbered version of the GNU General
595
+ Public License "or any later version" applies to it, you have the
596
+ option of following the terms and conditions either of that numbered
597
+ version or of any later version published by the Free Software
598
+ Foundation. If the Program does not specify a version number of the
599
+ GNU General Public License, you may choose any version ever published
600
+ by the Free Software Foundation.
601
+
602
+ If the Program specifies that a proxy can decide which future
603
+ versions of the GNU General Public License can be used, that proxy's
604
+ public statement of acceptance of a version permanently authorizes you
605
+ to choose that version for the Program.
606
+
607
+ Later license versions may give you additional or different
608
+ permissions. However, no additional obligations are imposed on any
609
+ author or copyright holder as a result of your choosing to follow a
610
+ later version.
611
+
612
+ 15. Disclaimer of Warranty.
613
+
614
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
615
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
616
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
617
+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
618
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
619
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
620
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
621
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
622
+
623
+ 16. Limitation of Liability.
624
+
625
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
626
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
627
+ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
628
+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
629
+ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
630
+ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
631
+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
632
+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
633
+ SUCH DAMAGES.
634
+
635
+ 17. Interpretation of Sections 15 and 16.
636
+
637
+ If the disclaimer of warranty and limitation of liability provided
638
+ above cannot be given local legal effect according to their terms,
639
+ reviewing courts shall apply local law that most closely approximates
640
+ an absolute waiver of all civil liability in connection with the
641
+ Program, unless a warranty or assumption of liability accompanies a
642
+ copy of the Program in return for a fee.
643
+
644
+ END OF TERMS AND CONDITIONS
645
+
646
+ How to Apply These Terms to Your New Programs
647
+
648
+ If you develop a new program, and you want it to be of the greatest
649
+ possible use to the public, the best way to achieve this is to make it
650
+ free software which everyone can redistribute and change under these terms.
651
+
652
+ To do so, attach the following notices to the program. It is safest
653
+ to attach them to the start of each source file to most effectively
654
+ state the exclusion of warranty; and each file should have at least
655
+ the "copyright" line and a pointer to where the full notice is found.
656
+
657
+ <one line to give the program's name and a brief idea of what it does.>
658
+ Copyright (C) <year> <name of author>
659
+
660
+ This program is free software: you can redistribute it and/or modify
661
+ it under the terms of the GNU General Public License as published by
662
+ the Free Software Foundation, either version 3 of the License, or
663
+ (at your option) any later version.
664
+
665
+ This program is distributed in the hope that it will be useful,
666
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
667
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
668
+ GNU General Public License for more details.
669
+
670
+ You should have received a copy of the GNU General Public License
671
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
672
+
673
+ Also add information on how to contact you by electronic and paper mail.
674
+
675
+ If the program does terminal interaction, make it output a short
676
+ notice like this when it starts in an interactive mode:
677
+
678
+ <program> Copyright (C) <year> <name of author>
679
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
680
+ This is free software, and you are welcome to redistribute it
681
+ under certain conditions; type `show c' for details.
682
+
683
+ The hypothetical commands `show w' and `show c' should show the appropriate
684
+ parts of the General Public License. Of course, your program's commands
685
+ might be different; for a GUI interface, you would use an "about box".
686
+
687
+ You should also get your employer (if you work as a programmer) or school,
688
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
689
+ For more information on this, and how to apply and follow the GNU GPL, see
690
+ <https://www.gnu.org/licenses/>.
691
+
692
+ The GNU General Public License does not permit incorporating your program
693
+ into proprietary programs. If your program is a subroutine library, you
694
+ may consider it more useful to permit linking proprietary applications with
695
+ the library. If this is what you want to do, use the GNU Lesser General
696
+ Public License instead of this License. But first, please read
697
+ <https://www.gnu.org/licenses/why-not-lgpl.html>.
trunk/printful-shipping.php ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ Plugin Name: Printful Integration for WooCommerce
4
+ Plugin URI: https://wordpress.org/plugins/printful-shipping-for-woocommerce/
5
+ Description: Connects your Printful account with WooCommerce.
6
+ Version: 2.1.32
7
+ Author: Printful
8
+ Author URI: http://www.printful.com
9
+ License: GPL3 https://www.gnu.org/licenses/gpl-3.0.en.html
10
+ Text Domain: printful
11
+ WC requires at least: 3.0.0
12
+ WC tested up to: 6.5.1
13
+ */
14
+
15
+ if ( ! defined( 'ABSPATH' ) ) exit;
16
+
17
+ if ( ! defined( 'PF_PLUGIN_FILE' ) ) {
18
+ define( 'PF_PLUGIN_FILE', __FILE__ );
19
+ }
20
+
21
+ class Printful_Base {
22
+
23
+ const VERSION = '2.1.32';
24
+ const PF_HOST = 'https://www.printful.com/';
25
+ const PF_API_HOST = 'https://api.printful.com/';
26
+
27
+ /**
28
+ * Construct the plugin.
29
+ */
30
+ public function __construct() {
31
+ add_action( 'plugins_loaded', array( $this, 'init' ) );
32
+ add_action( 'plugins_loaded', array( $this, 'printful_load_plugin_textdomain') );
33
+ }
34
+
35
+ /**
36
+ * Initialize the plugin.
37
+ */
38
+ public function init() {
39
+
40
+ if (!class_exists('WC_Integration')) {
41
+ return;
42
+ }
43
+
44
+ // WP REST API.
45
+ $this->rest_api_init();
46
+
47
+ //load required classes
48
+ require_once 'includes/class-printful-integration.php';
49
+ require_once 'includes/class-printful-carriers.php';
50
+ require_once 'includes/class-printful-taxes.php';
51
+ require_once 'includes/class-printful-shipping.php';
52
+ require_once 'includes/class-printful-request-log.php';
53
+ require_once 'includes/class-printful-admin.php';
54
+ require_once 'includes/class-printful-admin-dashboard.php';
55
+ require_once 'includes/class-printful-admin-settings.php';
56
+ require_once 'includes/class-printful-admin-status.php';
57
+ require_once 'includes/class-printful-admin-support.php';
58
+ require_once 'includes/class-printful-size-chart-tab.php';
59
+ require_once 'includes/class-printful-size-chart-tab.php';
60
+ require_once 'includes/class-printful-template.php';
61
+ require_once 'includes/class-printful-customizer.php';
62
+ require_once 'includes/class-printful-size-guide.php';
63
+
64
+ //launch init
65
+ Printful_Taxes::init();
66
+ Printful_Shipping::init();
67
+ Printful_Request_log::init();
68
+ Printful_Admin::init();
69
+ Printful_Size_Chart_Tab::init();
70
+ Printful_Template::init();
71
+ Printful_Customizer::init();
72
+ Printful_Size_Guide::init();
73
+
74
+ //hook ajax callbacks
75
+ add_action( 'wp_ajax_save_printful_settings', array( 'Printful_Admin_Settings', 'save_printful_settings' ) );
76
+ add_action( 'wp_ajax_ajax_force_check_connect_status', array( 'Printful_Integration', 'ajax_force_check_connect_status' ) );
77
+ add_action( 'wp_ajax_get_printful_stats', array( 'Printful_Admin_Dashboard', 'render_stats_ajax' ) );
78
+ add_action( 'wp_ajax_get_printful_orders', array( 'Printful_Admin_Dashboard', 'render_orders_ajax' ) );
79
+ add_action( 'wp_ajax_get_printful_status_checklist', array( 'Printful_Admin_Status', 'render_status_table_ajax' ) );
80
+ add_action( 'wp_ajax_get_printful_status_report', array( 'Printful_Admin_Support', 'render_status_report_ajax' ) );
81
+ add_action( 'wp_ajax_get_printful_carriers', array( 'Printful_Admin_Settings', 'render_carriers_ajax' ) );
82
+ }
83
+
84
+ /**
85
+ * Load Localisation files.
86
+ *
87
+ * Note: the first-loaded translation file overrides any following ones if the same translation is present.
88
+ */
89
+ public function printful_load_plugin_textdomain() {
90
+ load_plugin_textdomain( 'printful', false, plugin_basename( dirname( PF_PLUGIN_FILE ) ) . '/i18n/languages' );
91
+ }
92
+
93
+ /**
94
+ * @return string
95
+ */
96
+ public static function get_asset_url() {
97
+ return trailingslashit(plugin_dir_url(__FILE__)) . 'assets/';
98
+ }
99
+
100
+ /**
101
+ * @return string
102
+ */
103
+ public static function get_printful_host() {
104
+ if ( defined( 'PF_DEV_HOST' ) ) {
105
+ return PF_DEV_HOST;
106
+ }
107
+
108
+ return self::PF_HOST;
109
+ }
110
+
111
+ /**
112
+ * @return string
113
+ */
114
+ public static function get_printful_api_host() {
115
+ if ( defined( 'PF_DEV_API_HOST' ) ) {
116
+ return PF_DEV_API_HOST;
117
+ }
118
+
119
+ return self::PF_API_HOST;
120
+ }
121
+
122
+ private function rest_api_init()
123
+ {
124
+ // REST API was included starting WordPress 4.4.
125
+ if ( ! class_exists( 'WP_REST_Server' ) ) {
126
+ return;
127
+ }
128
+
129
+ // Init REST API routes.
130
+ add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 20);
131
+ }
132
+
133
+ public function register_rest_routes()
134
+ {
135
+ require_once 'includes/class-printful-rest-api-controller.php';
136
+
137
+ $printfulRestAPIController = new Printful_REST_API_Controller();
138
+ $printfulRestAPIController->register_routes();
139
+ }
140
+ }
141
+
142
+ new Printful_Base(); //let's go
trunk/readme.txt ADDED
@@ -0,0 +1,438 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ === Printful Integration for WooCommerce ===
2
+ Contributors: girts_u, kievins, kberzins
3
+ Tags: woocommerce, printful, drop shipping, shipping, shipping rates, fulfillment, printing, fedex, carriers, checkout, t-shirts
4
+ Requires at least: 5.3
5
+ Tested up to: 6.0
6
+ Requires PHP: 5.6
7
+ Stable tag: 2.1.32
8
+ License: GPLv3 or later
9
+ License URI: https://www.gnu.org/licenses/gpl-3.0.en.html
10
+
11
+ Grow your store with the top print-on-demand dropshipping plugin
12
+
13
+ == Description ==
14
+
15
+ = Partner with the #1 print-on-demand drop shipping plugin. Over 300,000 active customers and counting! =
16
+
17
+ With Printful, you can focus on building your store. We'll print, fulfill and ship your products for you. Simply upload your design, add it to your store, and we'll do the rest.
18
+
19
+ We offer more than 305+ products in various categories—apparel, home & living items, jewelry, accessories, and more! Once your orders start rolling in, we’ll fulfill and ship each one under your brand at our in-house and partner facilities, located globally.
20
+
21
+ Printful customers have already made more than $1 billion in sales, so start selling today!
22
+
23
+ = How does Printful work? =
24
+
25
+ When a customer buys something from your WooCommerce store, the order is automatically imported to Printful, where we fulfill and ship it to them under your brand. You can then see your order, revenue, and profit statistics on your WooCommerce dashboard.
26
+
27
+ We’re trusted to print over 1 million products monthly and we can't wait to print yours!
28
+
29
+ Learn more about the WooCommerce + Printful integration [here](https://www.printful.com/dashboard/woocommerce/register):
30
+
31
+ = With WooCommerce + Printful, you get: =
32
+
33
+ - Control over your profit: We charge you to cover production while you set your retail price, and what's left is your profit
34
+ - Automated tax settings and live shipping rates
35
+ - Free design templates: Get creative with our Design Maker
36
+ - Product personalization: Sell designs your customers can personalize themselves
37
+ - Warehousing & Fulfillment services: Store your inventory at our warehouse, and we'll fulfill all your orders
38
+ - Stress-free shipping: Lost shipments are on us, and we offer shipping methods with tracking
39
+ - Discounted samples: Order samples of your products with 20% off + free shipping to select destinations to ensure your customers are getting quality products.
40
+ - 24/7 support: Get in touch with us using chat or email
41
+
42
+ Still not convinced? Check out our [demo store](https://woocommerce-demo.printful.com/) to see what's possible!
43
+
44
+ == Installation ==
45
+ 1. Upload 'printful-shipping-for-woocommerce' to the '/wp-content/plugins/' directory
46
+ 1. Activate the plugin through the 'Plugins' menu in WordPress
47
+ 1. Click the "Connect" button or add your Printful API key manually to Printful->Settings tab
48
+ 1. Enable shipping rate calculation in WooCommerce->Settings->Shipping->Printful Shipping tab
49
+ 1. To automatically calculate taxes please check 'Enable taxes and tax calculations' under WooCommerce Tax settings.
50
+ 1. Then go to Printful->Settings tab and check 'Calculated for all products shipped to states where Printful applies sales tax'.
51
+
52
+ == Frequently Asked Questions ==
53
+
54
+ = How do I get Printful API key? =
55
+
56
+ Go to https://www.printful.com/dashboard/store , select your WooCommerce store, click "Edit" and then click "Enable API Access". Your API key will be generated and displayed there.
57
+
58
+ == Screenshots ==
59
+
60
+ 1. Connect to Printful
61
+ 2. Printful dashboard
62
+ 3. Integration settings
63
+ 4. Plugin status page
64
+ 5. Support page
65
+
66
+ == Upgrade Notice ==
67
+
68
+ = 2.1.32 =
69
+ Woocommerce compatibility raised to 6.5.1
70
+ WordPress compatibility raised to 6.0
71
+
72
+ = 2.1.31 =
73
+ Licence updated to GPLv3. WordPress compatibility raised to 5.9 and Woocommerce compatibility raised to 6.4
74
+
75
+ = 2.1.30 =
76
+ Additional billing setting check
77
+
78
+ = 2.1.29 =
79
+ WordPress compatibility raised to 5.8. Size guide bugfix
80
+
81
+ = 2.1.28 =
82
+ Woocommerce compatibility raised to 6.1
83
+
84
+ = 2.1.27 =
85
+ Woocommerce compatibility raised to 6.0. Size guide bug fix
86
+
87
+ = 2.1.26 =
88
+ Woocommerce compatibility raised to 5.9
89
+
90
+ = 2.1.25 =
91
+ Woocommerce compatibility raised to 5.5.
92
+
93
+ = 2.1.24 =
94
+ Woocommerce compatibility raised to 5.4. Minor bug fixes
95
+
96
+ = 2.1.23 =
97
+ Woocommerce compatibility raised to 5.3. Minor bug fixes
98
+
99
+ = 2.1.22 =
100
+ Woocommerce compatibility raised to 5.2
101
+
102
+ = 2.1.21 =
103
+ Advanced size guide translations fix
104
+
105
+ = 2.1.20 =
106
+ Woocommerce compatibility raised to 5.0
107
+
108
+ = 2.1.19 =
109
+ Woocommerce compatibility raised to 4.9
110
+
111
+ = 2.1.18 =
112
+ Woocommerce compatibility raised to 4.8
113
+
114
+ = 2.1.17 =
115
+ Woocommerce compatibility raised to 4.7
116
+
117
+ = 2.1.16 =
118
+ Woocommerce compatibility raised to 4.6
119
+
120
+ = 2.1.15 =
121
+ Fix translations
122
+
123
+ = 2.1.14 =
124
+ Advanced size guide feature
125
+
126
+ = 2.1.13 =
127
+ Woocommerce compatibility raised to 4.3
128
+
129
+ = 2.1.12 =
130
+ Woocommerce compatibility raised to 4.2
131
+
132
+ = 2.1.11 =
133
+ Woocommerce compatibility raised to 4.1
134
+
135
+ = 2.1.10 =
136
+ Improved shipping settings and status page, Woocommerce compatibility raised to 4.0
137
+
138
+ = 2.1.9 =
139
+ Improved support for WooCommerce 3.9 and other minor bug fixes
140
+
141
+ = 2.1.8 =
142
+ Improved support for WooCommerce 3.8 and other minor bug fixes
143
+
144
+ = 2.1.7 =
145
+ Updated labels in plugin settings
146
+
147
+ = 2.1.6 =
148
+ Added the ability to change personalization popup title
149
+
150
+ = 2.1.5 =
151
+ Fixed issues with personalization tool and other minor improvements
152
+
153
+ = 2.1.4 =
154
+ Fixed an issue with personalized order submit
155
+
156
+ = 2.1.3 =
157
+ Fixed minor issue with site URL verification in plugin status page
158
+
159
+ = 2.1.2 =
160
+ Improved theme support for personalized products and other minor bug fixes
161
+
162
+ = 2.1.1 =
163
+ Improved support for WooCommerce 3.6 and other minor bug fixes
164
+
165
+ = 2.1 =
166
+ Personalization feature
167
+
168
+ = 2.0.8 =
169
+ Shipping & Tax information improvements
170
+
171
+ = 2.0.7 =
172
+ Bug fixes, security improvements and improved i18n support
173
+
174
+ = 2.0.6 =
175
+ Improvement support for WordPress 5.0 and fixed various minor issues
176
+
177
+ = 2.0.5 =
178
+ Improvements to Printful shipping carrier settings
179
+
180
+ = 2.0.4 =
181
+ Improvements and fixed various minor issues
182
+
183
+ = 2.0.3 =
184
+ Fixed various minor issues, enabled localization, Improved compatibility with WC 3.4.3
185
+
186
+ = 2.0.2 =
187
+ Improved compatibility with WC 3.3.0
188
+
189
+ = 2.0.1 =
190
+ Fixed various minor issues
191
+
192
+ = 2.0 =
193
+ New major plugin version
194
+
195
+ = 1.2.8 =
196
+ Fixed bug that caused tax rates to become invisible on checkout since WC 3.0
197
+
198
+ = 1.2.7 =
199
+ Do not calculate shipping rates for US addresses while ZIP or state is not entered
200
+
201
+ = 1.2.6 =
202
+ Include shipping rates in tax calculation for states that require that
203
+
204
+ = 1.2.5 =
205
+ Added option to allow Woocommerce default rates together with Printful rates for Printful products
206
+
207
+ = 1.2.4 =
208
+ Prevent virtual products from requiring shipping rate when bought together with Printful products
209
+
210
+ = 1.2.3 =
211
+ Fixed issue introduced in 1.2.2
212
+
213
+ = 1.2.2 =
214
+ Fixed PHP warning on Woocommerce 2.6
215
+
216
+ = 1.2.1 =
217
+ Fixed bug
218
+
219
+ = 1.2 =
220
+ Support calculating shipping rates for both Printful and non-Printful products at the same time
221
+
222
+ = 1.1.2 =
223
+ Removed check for Curl extension
224
+
225
+ = 1.1.1 =
226
+ Ignore virtual and downloadable products when calculating shipping rates
227
+
228
+ = 1.1 =
229
+ Added tax rate calculation
230
+
231
+ = 1.0.2 =
232
+ Added option to disable SSL
233
+
234
+ = 1.0.1 =
235
+ Minor improvements
236
+
237
+ = 1.0 =
238
+ First release
239
+
240
+ == Changelog ==
241
+
242
+ = 2.1.32 =
243
+ Woocommerce compatibility raised to 6.5.1
244
+ WordPress compatibility raised to 6.0
245
+
246
+ = 2.1.31 =
247
+ * Licence updated to GPLv3
248
+ * WordPress compatibility raised to 5.9
249
+ * Woocommerce compatibility raised to 6.4
250
+
251
+ = 2.1.30 =
252
+ Additional billing setting check
253
+
254
+ = 2.1.29 =
255
+ * WordPress compatibility raised to 5.8. Size guide bugfix
256
+
257
+ = 2.1.28 =
258
+ * Woocommerce compatibility raised to 6.1
259
+
260
+ = 2.1.27 =
261
+ * Woocommerce compatibility raised to 6.0
262
+ * Size guide bug fix
263
+
264
+ = 2.1.26 =
265
+ * Woocommerce compatibility raised to 5.9
266
+
267
+ = 2.1.25 =
268
+ * Woocommerce compatibility raised to 5.5
269
+
270
+ = 2.1.24 =
271
+ * Woocommerce compatibility raised to 5.4
272
+
273
+ = 2.1.23 =
274
+ * Woocommerce compatibility raised to 5.3
275
+ * Minor bug fixes
276
+
277
+ = 2.1.22 =
278
+ * Woocommerce compatibility raised to 5.2
279
+
280
+ = 2.1.21 =
281
+ * Advanced size guide translations fix
282
+
283
+ = 2.1.20 =
284
+ * Woocommerce compatibility raised to 5.0
285
+
286
+ = 2.1.19 =
287
+ * Woocommerce compatibility raised to 4.9
288
+
289
+ = 2.1.18 =
290
+ * Woocommerce compatibility raised to 4.8
291
+
292
+ = 2.1.17 =
293
+ * Woocommerce compatibility raised to 4.7
294
+
295
+ = 2.1.16 =
296
+ * Woocommerce compatibility raised to 4.6
297
+
298
+ = 2.1.15 =
299
+ * Fix translations
300
+
301
+ = 2.1.14 =
302
+ * Advanced size guide feature
303
+
304
+ = 2.1.13 =
305
+ * Improved support for WooCommerce 4.3
306
+
307
+ = 2.1.12 =
308
+ * Improved support for WooCommerce 4.2
309
+
310
+ = 2.1.11 =
311
+ * Improved support for WooCommerce 4.1
312
+
313
+ = 2.1.10 =
314
+ * Improved support for WooCommerce 4.0 and other minor bug fixes
315
+
316
+ = 2.1.9 =
317
+ * Improved support for WooCommerce 3.9 and other minor bug fixes
318
+
319
+ = 2.1.8 =
320
+ * Improved support for WooCommerce 3.8 and other minor bug fixes
321
+
322
+ = 2.1.7 =
323
+ * Updated labels in plugin settings
324
+
325
+ = 2.1.6 =
326
+ * Added the ability to change personalization popup title
327
+
328
+ = 2.1.5 =
329
+ * Fixed issues with personalization tool and other minor improvements
330
+
331
+ = 2.1.4 =
332
+ * Fixed an issue with personalized order submit
333
+
334
+ = 2.1.3 =
335
+ * Fixed minor issue with site URL verification in plugin status page
336
+
337
+ = 2.1.2 =
338
+ * Improved theme support for personalized products and other minor bug fixes
339
+
340
+ = 2.1.1 =
341
+ * Improved support for WooCommerce 3.6 and other minor bug fixes
342
+
343
+ = 2.1 =
344
+ * Personalization feature
345
+
346
+ = 2.0.8 =
347
+ * Shipping & Tax information improvements
348
+
349
+ = 2.0.7 =
350
+ * Bug fixes, security improvements and improved i18n support
351
+
352
+ = 2.0.6 =
353
+ * Improvement support for WordPress 5.0 and fixed various minor issues
354
+
355
+ = 2.0.5 =
356
+ * Improvements to Printful shipping carrier settings
357
+
358
+ = 2.0.4 =
359
+ * Improved support for older PHP versions
360
+ * Other minor bug fixes
361
+
362
+ = 2.0.3 =
363
+ * Improved compatibility with WC 3.4.3
364
+ * Wordpress localization support
365
+ * Minor bug fixes
366
+
367
+ = 2.0.2 =
368
+ * Fixed incorrect webhook status indication since WC 3.3.0
369
+
370
+ = 2.0.1 =
371
+ * Improved Printful connection status detection
372
+ * Improvements for system report
373
+ * Show warning if attempting to connect from localhost
374
+
375
+ = 2.0 =
376
+ * New major plugin version
377
+ * All new Printful dashboard
378
+ * Connect to Printful with a single click
379
+ * View your Printful profits and latest Printful product orders in WordPress admin
380
+ * Edit your shipping carriers from Printful dashboard
381
+ * Improved sales tax compatibility with existing tax rates
382
+ * New status page - see if your integration is running smoothly
383
+ * New support page - all info about finding help in one place
384
+ * Size chart tab - when pushing products from Printful, the size chart will be placed in a separate tab
385
+ * Improved logging of API requests coming to and from Printful
386
+
387
+ = 1.2.8 =
388
+ * Fixed bug that caused tax rates to become invisible on checkout since WC 3.0
389
+
390
+ = 1.2.7 =
391
+ * Do not calculate shipping rates for US addresses while ZIP or state is not entered
392
+
393
+ = 1.2.6 =
394
+ * Include shipping rates in tax calculation for states that require that
395
+
396
+ = 1.2.5 =
397
+ * Added option to allow Woocommerce default rates together with Printful rates for Printful products
398
+
399
+ = 1.2.4 =
400
+ * Prevent virtual products from requiring shipping rate when bought together with Printful products
401
+
402
+ = 1.2.3 =
403
+ * Fixed issue introduced in 1.2.2
404
+
405
+ = 1.2.2 =
406
+ * Fixed PHP warning on Woocommerce 2.6 due to changed method signature
407
+ * Fixed conflict with "Multiple Packages for WooCommerce" plugin
408
+
409
+ = 1.2.1 =
410
+ * Fixed bug that could have show error message when calculating shipping rates
411
+
412
+ = 1.2 =
413
+ * Support calculating shipping rates for both Printful and non-Printful products at the same time (non-Printful
414
+ products will get default rates provided by Woocommerce)
415
+ * Added caching to tax rates
416
+ * Improved compatibility with Woocommerce 2.6
417
+
418
+ = 1.1.2 =
419
+ * Removed check for Curl extension (since we already used wp_remote_get and it is no longer necessary)
420
+
421
+ = 1.1.1 =
422
+ * Ignore virtual and downloadable products when calculating shipping rates
423
+
424
+ = 1.1 =
425
+ * Added option to calculate sales tax rates for locations where it is required for Printful orders
426
+ * Added automatic conversion of shipping rates to the currency used by Woocommerce
427
+ * Printful API client library updated to use Wordpress internal wp_remote_get method instead of CURL directly
428
+ * Changed plugin code structure for easier implementation of new features in the future
429
+
430
+ = 1.0.2 =
431
+ * Added option to disable SSL for users that do not have a valid CA certificates in their PHP installation
432
+
433
+ = 1.0.1 =
434
+ * Removed CURLOPT_FOLLOWLOCATION that caused problems on some hosting environments
435
+ * Added option to display reason status messages if the rate API request has failed
436
+
437
+ = 1.0 =
438
+ * First release